# pipeloom **Repository Path**: hukanfa/pipeloom ## Basic Information - **Project Name**: pipeloom - **Description**: 自托管 DevOps 平台:流水线编排 · 代码扫描 · 制品晋级 · 部署发布 · 服务器/容器运维。 - **Primary Language**: Unknown - **License**: Not specified - **Default Branch**: main - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 0 - **Forks**: 20 - **Created**: 2026-10-21 - **Last Updated**: 2026-10-21 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## README
# Pipeloom **A self-hosted DevOps platform: pipeline orchestration · code scanning · artifact promotion · deployment · server & container ops.** Kotlin / Spring Boot control plane, Vue 3 console, MySQL as the single source of state — plus a tiny Go probe for the hosts you manage. ![Kotlin](https://img.shields.io/badge/Kotlin-2.3-A97BFF?logo=kotlin&logoColor=white) ![Spring Boot](https://img.shields.io/badge/Spring%20Boot-4.1-6DB33F?logo=springboot&logoColor=white) ![Vue](https://img.shields.io/badge/Vue-3.5-42B883?logo=vue.js&logoColor=white) ![MySQL](https://img.shields.io/badge/MySQL-8.0-4479A1?logo=mysql&logoColor=white) ![License: Private](https://img.shields.io/badge/license-Private-lightgrey) English | [简体中文](README.zh-CN.md)
--- ## Why Pipeloom A typical internal delivery stack is an assembly: a CI server, a SonarQube box, Ansible playbooks, a Portainer tab, a spreadsheet of "who approved this release", and someone's cron script for alerts. Every hand-off between those tools loses context, and nobody can answer "what is running in prod right now, and who approved it". Pipeloom puts the whole loop in one control plane: the pipeline that pulls the code, the scanners that grade it, the artifact that promotion approval gates, the deploy that ships it, and the ops console you land in when it breaks. The node catalogue, the scan metrics and the credential vault are shared across all of it — a quality gate reads the very numbers the scan node produced, a deploy refuses to start until the artifact is approved for that environment, and every write ends up in one tamper-evident audit trail. | | Pipeloom | Jenkins | GitLab CI + SonarQube + Ansible | In-house scripts + cron | |---|:---:|:---:|:---:|:---:| | Visual pipeline canvas (Stage→Job DAG) | ✅ built-in | plugin | `.gitlab-ci` only | ❌ | | Code scanning + red-line gates | ✅ 14 engines + built-in rules | plugins | separate SonarQube | ❌ | | Artifact promotion approval (per environment) | ✅ enforced server-side | ❌ | ❌ | ❌ | | Multi-target deploy (SSH / Compose / Kubernetes) | ✅ built-in | plugin | Ansible | hand-written | | Server & container ops + web terminal | ✅ built-in | ❌ | Portainer | ❌ | | Agentless SSH **and** an outbound-only probe | ✅ | ❌ | Ansible only | ❌ | | Plugin marketplace (hot-swappable nodes, no restart) | ✅ | ✅ | ❌ | ❌ | | Credential vault (AES-256-GCM, ciphertext only) | ✅ | plugin | CI variables | env files | | Append-only audit (DB triggers block UPDATE/DELETE) | ✅ | plugin | ❌ | ❌ | | RBAC (ADMIN/OPERATOR/VIEWER) + scoped API tokens | ✅ | plugin | ✅ | ❌ | | Ops footprint | 1 jar + 1 static site + MySQL | JVM + plugins | 3+ services | whatever you kept | ## Console tour Screenshots are included in this repo — the pages you land on: - **Overview** — pipeline / server / probe counts, credentials, 24h runs with success rate, trends, recent activity. ![alt text](docs/screenshots/overview.png) - **Pipelines** — the two-level canvas (stages left→right, jobs stacked inside a stage), the node drawer with dynamic fields, YAML import, per-version config history with rollback. ![alt text](docs/screenshots/pipeline.png) - **Runs** — stage timeline, per-step status, live job logs (server flushes step logs every 2s, console polls at 2s), reports, artifacts, approval actions, rerun / cancel / force-kill. ![alt text](docs/screenshots/run-detail.png) - **Servers & Containers** — SSH and Kubernetes targets, aggregate container view, images / volumes / networks / stacks, `system df` + prune, live stats, web terminals. ![alt text](docs/screenshots/server-and-container.png) - **Code scanning & quality** — tool matrix per node, scan rule inventory, quality metrics, red-line rules, promotion approvals. ![alt text](docs/screenshots/artifact-report.png) ![alt text](docs/screenshots/quality-redline.png) - **Marketplace** — plugin packages, versions, per-user installs, comments, the built-in plugin development guide and Kotlin templates. ![alt text](docs/screenshots/plugin-marketplace.png) - **Settings** — users & roles, notification channels, API tokens, audit log, AI provider, account, plus the in-app message centre in the header. ![alt text](docs/screenshots/setting.png) ## Feature overview - **🔐 Security foundation** — argon2id password hashing, RSA-OAEP encrypted password transport at login, CSRF token per session, login lockout (5 attempts / 15 min by default), session cookies with an opt-in `Secure` flag, Bearer API tokens (`pipeline:trigger` scope), RBAC with `ADMIN` / `OPERATOR` / `VIEWER` enforced on the server: reads are open to any signed-in user, **every write defaults to OPERATOR+** (a bare `@RequireAuth` no longer lets a VIEWER through), while platform-level actions — revealing credential plaintext, issuing API tokens, user management, plugins and the rule library, reverse-proxy routes, schedule refresh, `docker system prune` — are explicitly ADMIN-only. Tokens whose scope already matched the path keep working. `AuthContractTest` + `AuthInterceptorRoleTest` + `PipelineVisibilityTest` pin the policy down (a new anonymous endpoint must be registered with a reason, and someone else's private pipeline has to vanish from lists, detail views and run history alike), and the UI mirrors it per role. - **🗝 Credential vault** — git token / git HTTP / SSH key / SSH password / registry / kubeconfig, encrypted with AES-256-GCM under `PIPELOOM_VAULT_KEY` (base64, 32 bytes) and stored as `nonce ‖ ciphertext ‖ tag`. Plaintext never lands in the DB and never comes back in a GET. Without the key, vault-backed features answer `503 vault_unconfigured` instead of half-working. Credential expiry is scanned hourly, with a 7-day warning. - **🧩 Pipeline orchestration** — Stage→Job two-level DAG, `needs` dependencies, topological levels where stages in the same level run concurrently (jobs inside a stage run in declaration order unless `job.needs` reorders them, and an upstream that failed or was skipped makes its dependents skip), stage-level and job-level `allowFailure`, stage/job `when` conditions (branch patterns, trigger events, expressions), per-job retries on every node type with exponential backoff (gate nodes — `approval` / `quality_gate` / `concurrency_group` — are never retried), per-job timeouts enforced as a shared budget across retries (see Known boundaries), stage-level approval gates and the standalone `approval` node, per-stage container `image` plus `cachePaths` / `cacheKey` dependency caching, per-stage isolated working directories via `isolatedWorkspace`, build machine binding by `agentId` or `agentLabels`, matrix builds with `matrix` / `matrixExclude` / `matrixInclude` / `matrixFailFast`. - **🚦 Triggers** — manual, cron schedule (5-field expressions are normalised to Spring's 6-field form), Git webhooks from GitHub / GitLab / Gitea with push · tag · PR/MR · release event parsing, a generic webhook endpoint, pipeline chaining (`call_pipeline`, self-call rejected, outputs propagated back), REST trigger with a Bearer token. Inbound webhooks are HMAC-verified (`X-Pipeloom-Signature` + timestamp, `X-Hub-Signature-256`, or `X-Gitlab-Token`), deduplicated by delivery id for 5 minutes, IP-rate-limited after repeated signature failures, and auto-disabled after 10. Terminal runs are written back as a commit status on GitHub / GitLab / Gitea (best-effort — a failure there never changes the run result), so the PR shows ✓/✗ without opening the platform. Write-back needs a webhook-triggered run (a commit to mark) plus a `git_source` node carrying `repoUrl` and a **git_token** credential — public repos clone anonymously but cannot be written to without a token — and every reason for not writing back is logged as its own named outcome. - **🏗 Build, artifacts, promotion** — `git_source` clones into the workspace (or into the stage's container) with credentials embedded from the vault, and captures commit, author, file count and per-file diffs; JGit backs repo browsing and remote ref listing. `build_image` / `push_image` tag `pipeloom-:` and push to a private registry; `archive_artifact` keeps the real bytes of the build output as a downloadable artifact; per-run workspaces are cleaned up when the run reaches a terminal state. Deploy nodes that move artifacts (`deploy_ssh`, `file_distribute`) can require **promotion approval per (artifact, environment)**: approval is on the artifact, not the run, and a missing or incomplete gate **fails closed** while registering a pending request the approver can act on from the run page. - **🔍 Code scanning & quality red-lines** — the `code_scan` node orchestrates 14 registered engines. The built-in rule engine (24 style rules + 16 secret rules) is pure JVM and always available; gitleaks · SpotBugs · Checkstyle · PMD · detekt · ktlint · ESLint · Pylint · cpplint · clang · gometalinter · StyleCop · OcCheck each resolve through a `PATH → Docker image → skip` chain, and the skip is reported instead of silently producing nothing. `semgrep_scan` and `tca_scan` are separate nodes precisely because they need an external runtime (a Semgrep environment, or a reachable TCA server). Output is SARIF plus aggregated issue counts; scan results become run variables (`CODE_SCAN_TOTAL`, `CODE_SCAN_ERRORS`, `CODE_COMPLEXITY_MAX`, `CODE_DUPLICATION_RATIO`, `CODE_LOC_CODE`, …) that the `quality_gate` node evaluates against your red-line rules — fail the stage, or block for approval. Reports, per-file drill-down and a JSON export live on the run. - **🚀 Deployment** — `deploy_ssh` (load image, run container, ports + run args), `deploy_compose`, `file_distribute`, `server_script` for command-style ops, `health_check` with HTTP or command probes, retries and fatal/non-fatal semantics. Kubernetes targets support `image_update` (rolling · recreate · blue-green), `manifest_apply` (server-side apply with an explicit `fieldManager`, cluster-scoped kinds ordered first) and `helm_chart` (`helm upgrade --install` with values). - **🖥 Server & container ops** — SSH, Kubernetes and local targets plus probe-backed hosts; aggregated container list across hosts, create / inspect / prune, image · volume · network · stack management, `system df` and prune, live stats, service (systemd) start/stop/restart/status behind a command whitelist, host and container web terminals, Kubernetes pod exec, streaming log tails. - **🛰 Probe (agent)** — a single static `CGO_ENABLED=0` Go binary that dials **out** to `GET /ws/agents/connect?token=…` and never listens on a port. It reports OS · arch · CPU cores · memory · hostname on each heartbeat, executes `exec` / `upload` / `download` tasks with streaming output, and is classified `deploy` or `build` with tags and label-based scheduling. `SshExecutor` routes to the probe when a host is bound to one, and fails loudly when the probe is offline. The server serves the matching binary at `GET /api/agents/binary/{os}/{arch}`. - **📣 Notifications** — WeCom · DingTalk · Feishu · Slack · Microsoft Teams · generic webhook · email, routed by explicit event subscription (`succeeded`, `failed`, `gate_pending`, `canceled`, `custom`, `concurrency_rejected`, `gate_rejected` — success is opt-in on purpose, failure and approval are opt-out), template rendering with run variables, a per-delivery ledger, one-click **redelivery that re-renders with the current channel config**, and a test-send button. There is also a `notify` node inside the pipeline. - **📥 In-app inbox** — the same events reach an in-site message centre, and it does **not** depend on any channel being configured: a fresh instance with zero webhooks still notifies people. Fan-out is decided by event semantics rather than channel subscription — the pipeline owner and the run's trigger actor always get a copy, failures additionally reach ADMIN, and approval-type events (`gate_pending` / `gate_rejected` / `ticket_open`) reach ADMIN and OPERATOR because those are exactly the roles allowed to approve. Every recipient gets **their own row with their own read state** (Alice reading a gate request must not clear Bob's badge), names that are not live enabled accounts (API tokens, deleted owners, Git commit authors) are dropped, one event is capped at 200 copies, and the header bell plus `/messages` page render it. Inbox rows are deleted together with their run by the retention sweeper. - **🧩 Plugin marketplace & SDK** — plugins are `task.json` + a Kotlin/Java fat JAR (or a shell handler script). Installing registers a new node type in `JobHandlerRegistry`, or a new rule in `ScanRuleRegistry`, **without a restart**; uninstalling retires the type so old pipelines fail with an explanation instead of silently succeeding. Installations are per user, versions are checksum-verified and path-traversal-safe, reserved node types are refused, and plugins reach the platform through a one-time run-scoped API token that can only fetch credentials the pipeline itself declares. Five templates ship (four Kotlin plus one shell) together with an in-app development guide and a `plg` scaffold whose local checks share the server's rule implementation. - **🤖 AI assist (optional, fully degradable)** — any OpenAI-compatible endpoint (`PIPELOOM_AI_BASE_URL` / `MODEL` / `API_KEY`, so DeepSeek · Moonshot · vLLM work too; the key lives in the vault): pipeline generation from the repo, run diagnosis, issue fix suggestions, and the ops assistant (natural language → shell command, command explanation, compose generation, container diagnosis). Unconfigured or unreachable means `available=false` and the UI degrades quietly — the CI/CD path never depends on AI. - **🧹 Housekeeping & platform** — MySQL 8 + Flyway (`V0002`→`V0140`, 95 migrations) with MyBatis-Plus, run retention sweeper (default 90 days), two-layer crash recovery (`StaleRunRecoveryService` finalizes orphaned `running` / `queued` / `canceling` runs on boot, and a 2-minute watchdog terminalizes any run whose step heartbeat stopped for 3 hours), gate timeout enforcement, expired slot cleanup, i18n console (简体中文 / English), `/api/health`, Actuator `health,info,prometheus,metrics`, and Swagger UI at `/swagger-ui.html`. - **🩺 Run diagnosis & business metrics** — the "运行诊断" tab on a run answers *why is it slow / where is it stuck / is it dead*: queue time separated from execution time, heartbeat silence as a liveness verdict, exhausted per-job time budget, who the gate is waiting for, the first failed step with its last log line and whether `allowFailure` tolerates it, the bottleneck step's share of total time, and platform interrupt codes translated into what they mean. Every threshold lives in one server-side place (`RunDiagnosisService`); the UI only renders. `/actuator/prometheus` now carries pipeline business metrics next to the framework's own JVM/HTTP series: terminal counts by status and trigger, queue and duration distributions, retries, budget exhaustion, `when`/`needs` skips, gate approvals and rejections, matrix fan-out sizes, Git status outcomes, promotion blocks, and an active-runs gauge. Labels are always finite sets from code — never `pipelineId`, `runId`, usernames or step names, because those belong in the per-run diagnosis endpoint. - **⚖️ Concurrency control** — pipeline-level mutual exclusion groups (`deploy-prod`, `deploy-${branch}` …) that reject a conflicting trigger with a `409` naming the occupying run, and stage-level resource slots leased by the `concurrency_group` node with expiry sweeping. > Security is non-negotiable: secrets are ciphertext at rest, commands are whitelisted or arrayified, webhooks are signed, sessions and tokens are scoped, and audit rows cannot be updated or deleted. ## Install / deploy Requirements: **JDK 25 for the server toolchain** (Gradle downloads it if absent; the plugin SDK targets 17 so plugins stay portable), **MySQL 8.0+**, **Node.js 22+** for the console, and Docker only where you actually use it — container-isolated builds, image deploys and container ops. ### ① Docker Compose (recommended for self-hosting) ```bash git clone https://gitee.com/Adnois/pipeloom.git && cd pipeloom cp .env.example .env # set DB / admin / vault secrets before the first up docker compose up -d --build # mysql + server + web open http://localhost:8092 # login: admin / $PIPELOOM_ADMIN_PASSWORD ``` The `web` service is nginx: static console on port `8092`, `/api/` proxied to `server:8888`, `/ws/` proxied with an upgrade header and a 24h read timeout. Data lives in the `mysql-data`, `artifacts` and `plugins` named volumes. The server image runs as UID `10001` (non-root), aligned with the Kubernetes manifests. ### ② Kubernetes ```bash kubectl apply -f k8s/namespace.yaml # edit k8s/secret.yaml first — DB password, initial admin password, vault key kubectl apply -f k8s/secret.yaml kubectl apply -f k8s/mysql.yaml kubectl apply -f k8s/server.yaml # PVCs: 20Gi artifacts, 5Gi plugins kubectl apply -f k8s/web.yaml kubectl apply -f k8s/ingress.yaml # set your host, enable the TLS block ``` `k8s/server.yaml` pins `SERVER_PORT=8888` on purpose: Kubernetes injects `SERVER_PORT=tcp://…` for the Service named `server`, which Spring Boot's relaxed binding would otherwise read as `server.port` and fail to start. Liveness and readiness probe `/api/health`. Keep `replicas: 1` — cron scheduling and in-flight runs are driven from the process (see [Boundaries](#known-boundaries-honest-list)). ### ③ From source (development) ```bash # 1. database mysql -u root -p -e "CREATE DATABASE pipeloom CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;" # 2. backend — http://localhost:8888, Flyway migrates on boot cd server && ./gradlew bootRun # 3. console — http://localhost:5173, proxies /api and /ws to :8888 cd web && npm ci && npm run dev ``` Secrets come from environment variables, or from `server/src/main/resources/application-local.yml` (copy `application-local.yml.example`, it is git-ignored). Produce artifacts with `./gradlew bootJar` (→ `server/target/pipeloom-server.jar`) and `npm run build` (→ `web/dist`). The probe is a separate Go module: ```bash cd agent && make # pipeloom-agent-linux-amd64 / -linux-arm64 / -darwin-amd64 / -darwin-arm64 # Console → Agents → New agent → copy the connect command ./pipeloom-agent-darwin-arm64 -server=http://localhost:8888 -token= -v ``` ### Configuration (environment variables) `.env` really only needs three values — the DB password, the initial admin password and the vault key — plus `PIPELOOM_API_URL` once you serve it on a real host. Everything else has a usable default. **Core** | Variable | Description | Default | |---|---|---| | `PIPELOOM_ADMIN_PASSWORD` | Initial admin password, **used only on first launch** (change it later in account settings). Minimum 8 chars. No default — the project's own rule for password properties is fail fast rather than shipping an instance nobody can log into | none (required on first launch) | | `PIPELOOM_ADMIN_USERNAME` | Admin username created on first launch | `admin` | | `PIPELOOM_VAULT_KEY` | Base64-encoded 32-byte vault master key. Unset ⇒ credentials, terminals, K8s exec, AI config and all AI features are unavailable (`503 vault_unconfigured`) | none | | `PIPELOOM_API_URL` | Externally reachable base URL — used in notification links and webhook `curl` examples | `http://localhost:8888` | | `PIPELOOM_AUTH_COOKIE_SECURE` | Set `true` once you serve over HTTPS | `false` | | `SERVER_PORT` | HTTP port | `8888` | **Database** | Variable | Description | Default | |---|---|---| | `PIPELOOM_DB_HOST` | MySQL host | `localhost` | | `PIPELOOM_DB_USERNAME` | MySQL user | `root` | | `PIPELOOM_DB_PASSWORD` | MySQL password. Compose reads it from `.env`, K8s from the Secret — always set it explicitly; note `application.yml` still carries a local-development fallback that production must never rely on | env var (`.env` for Compose) | **Runs, plugins & retention** | Variable | Description | Default | |---|---|---| | `PIPELOOM_RETENTION_RUNS_MAX_AGE_DAYS` | Days a terminal run is kept before the sweeper removes it with its logs, artifacts, reports, notification rows and in-app copies | `90` | | `PIPELOOM_PLUGIN_STORAGE` | Directory for uploaded plugin packages | `./data/plugins` | | `PIPELOOM_PLUGIN_EXEC_TIMEOUT_SECONDS` | Default plugin execution ceiling (a node may override it, clamped 1s–24h) | `3600` | | `pipeloom.agent-binary-dir` | Where the server looks for probe binaries to serve | `./agent` | | `pipeloom.server.local-bootstrap.enabled` | Auto-register the host it runs on as a `local` target | `true` | | `pipeloom.security.ws.allowed-origins` | WebSocket origin allow-list (CSWSH guard) — add your console origin when you deploy | `localhost` / `127.0.0.1` patterns | **AI (optional)** | Variable | Description | Default | |---|---|---| | `PIPELOOM_AI_BASE_URL` | OpenAI-compatible base URL | `https://api.openai.com/v1` | | `PIPELOOM_AI_API_KEY` | API key; empty ⇒ AI disabled (startup is never blocked) | empty | | `PIPELOOM_AI_MODEL` | Model name | `gpt-4o-mini` | | `PIPELOOM_AI_TIMEOUT_SECONDS` | Request timeout | `120` | ## Pipeline reference Node types available on the canvas, and who executes them: | Category | Node | Executor | |---|---|---| | Source | `trigger` | engine (fixed first stage: events, cron, concurrency) | | Source | `git_source` | engine (`git clone` + commit/diff capture); JGit for repo browsing | | Build | `build_image`, `push_image`, `archive_artifact` | engine (Docker / registry credentials from the vault) | | Build | `script`, `custom`, `test` | **platform-reserved** — install a marketplace plugin | | Deploy | `deploy_ssh`, `deploy_compose`, `file_distribute`, `server_script` | built-in handlers (SSH or probe transport) | | Deploy | `deploy_k8s` | built-in handler (fabric8: image rollout / manifests / Helm) | | Quality | `code_scan`, `semgrep_scan`, `tca_scan`, `quality_gate`, `health_check` | built-in handlers | | Flow | `approval`, `concurrency_group`, `call_pipeline` | built-in handlers | | Notify | `notify` | built-in handler (routes through your channels) | `script` / `custom` / `test` are deliberately kept as reserved types: they exist so older pipelines do not turn red overnight, but they are **no-ops** unless a plugin provides them. The canvas and the save-time validator say so instead of letting you discover it mid-run — and the run page badges a succeeded placeholder step as 「空操作」 (no-op), so a green check never claims work that did not happen. Built-in variables available to commands, env values and node config (`${VAR}` and `${VAR:-default}`): `RUN_ID`, `PIPELINE_ID`, `PIPELINE_NAME`, `BRANCH`, `COMMIT_SHA`, `COMMIT_SHORT`, `TRIGGER_TYPE`, `TRIGGER_ACTOR`, `STAGE_NAME`, `JOB_NAME`, `WORKSPACE`, plus anything a node publishes through run variables (artifact names, scan metrics, matrix axes, sub-pipeline outputs). ## Plugin marketplace in short ```json { "name": "hello-world", "description": "Hello World 示例插件", "nodeType": "hello_world", "version": "1.0.0", "category": "custom", "execution": { "language": "java", "packagePath": "build/libs/hello-world-1.0.0.jar" }, "fields": [{ "key": "message", "label": "消息内容", "kind": "text" }], "outputs": [{ "name": "RESULT", "desc": "输出结果" }], "defaultConfig": { "message": "Hello, Pipeloom!" } } ``` ```bash gradle jar zip hello-plugin.zip task.json build/libs/hello-world-1.0.0.jar # Console → Plugin marketplace → Upload package (drag & drop works) → Install ``` A plugin implements `PluginTask

` from `server/plugin-sdk`; the platform injects config as `PLG_*` environment variables plus `$PLG_INPUT_FILE`, and reads back `$PLG_OUTPUTS_FILE` (variables), `$PLG_REPORT_FILE` (structured report) and `$PLG_API_URL` + `$PLG_API_TOKEN` (a one-time, run-scoped token that is revoked when the run reaches a terminal state). Templates: `tpl-kotlin-basic`, `tpl-kotlin-deploy`, `tpl-kotlin-notify`, `tpl-kotlin-scan`. A plugin is just a `java -jar` process driven by `PLG_*` env vars, so it can be developed and iterated on **without the platform**: point `PLG_INPUT_FILE` at a JSON file, read results from `PLG_OUTPUTS_FILE` / `PLG_REPORT_FILE`, and use `PLG_CREDENTIALS_FILE` to fake credentials (honoured only while `PLG_API_URL` is unset, so a pipeline variable can never redirect a plugin to an arbitrary file on the build host). Exit codes match the platform's verdict: 0 success, 1 failure, 2 error. Each template README ships the exact command, and every template now carries a runnable unit test suite (`gradle test`) that drives `execute()` through `ctx.result` assertions. ## Tech stack - **Backend** — Kotlin 2.3 · Spring Boot 4.1 (web, websocket, mail, flyway, actuator) · MyBatis-Plus 3.5 · MySQL 8 + Flyway · BouncyCastle · sshj (SSH) · fabric8 `kubernetes-client` · Eclipse JGit · Jackson 3 (`tools.jackson`) · springdoc OpenAPI · Micrometer/Prometheus · ArchUnit for layering guards · ktlint + detekt in the build. - **Console** — Vue 3.5 `