-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` | engine (container or local shell, default image `alpine`, `commands` interpolated) |
| Build | `test` | **platform-reserved placeholder** — no-op unless a plugin provides it (“no-op” badge on the step) |
| 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`, `dependency_scan`, `quality_gate`, `health_check` | built-in handlers |
| AI | `ai_design`, `ai_coding`, `ai_test`, `ai_wiki` | built-in handlers (fail loudly when AI is unconfigured) |
| Flow | `approval`, `concurrency_group`, `call_pipeline` | built-in handlers |
| Notify | `notify`, `webhook` | built-in handler (`webhook` calls an external HTTP service, sync or async callback) |
`test` is deliberately kept as a reserved type: it exists so older pipelines do not turn red overnight, but it is a **no-op** (beyond archiving whatever `artifactPath` names) unless a plugin provides it. 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. `script` and `custom` are not placeholders: they really run `commands` inside a container (default `alpine`, overridable per stage or job) or in the local shell.
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 `