# pipeloom **Repository Path**: Adnois/pipeloom ## Basic Information - **Project Name**: pipeloom - **Description**: 自托管 DevOps 平台:流水线编排 · 代码扫描 · 制品晋级 · 部署发布 · 服务器/容器运维。 - **Primary Language**: Unknown - **License**: Not specified - **Default Branch**: main - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 37 - **Forks**: 15 - **Created**: 2026-09-24 - **Last Updated**: 2026-11-01 ## Categories & Tags **Categories**: DevOps **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)
--- ## Try it live - Entry point: - Account: `dadmin` / `dadmin123` The demo runs behind a Cloudflare Quick Tunnel: **the URL changes whenever the tunnel restarts**, so treat this line as the source of truth and update it in place rather than forwarding an old link. The account is a public demo account — anyone with the link can sign in — so don't put real code, real credentials or production secrets on it. ## 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 included) | 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 (SUPER_ADMIN / TENANT_ADMIN / OPERATOR / DEVELOPER / VIEWER) + scoped API tokens | ✅ | plugin | ✅ | ❌ | | Multi-tenant isolation (ownership · visibility · grant list) | ✅ enforced server-side | ❌ | partial (groups) | ❌ | | AI workstations (design · code · test · repo wiki · self-heal tickets) | ✅ optional, fully degradable | plugin | ❌ | ❌ | | Ops footprint | 1 jar + 1 static site + MySQL | JVM + plugins | 3+ services | whatever you kept | ## Console tour Screenshots are included in this repo, in the order you'd use them: - **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; the "run diagnosis" tab turns *why is it slow / where is it stuck / is it dead* into verdicts, and a failed step shows the real cause with its exception chain unwrapped instead of a bare `ExecutionException`. ![alt text](docs/screenshots/run-detail.png) - **Repos** — register Git repositories (URL, credential, default branch) and mark them public / private; open one to browse sources by branch and remote ref. The credential comes from the vault, and public repos can leave it empty. ![alt text](docs/screenshots/projects.png) - **Build agents & probes** — two kinds of build agent: container agents spawn a one-shot container inside the target Docker daemon, host agents run over SSH with a resident toolchain; both bind to pipelines via `agentId` / `agentLabels`. The probe page handles enrolment of the outbound Go binary — kind (`deploy` / `build`), tags, live heartbeat — and the token is shown only on create and on reset. ![alt text](docs/screenshots/build-agents.png) ![alt text](docs/screenshots/agents.png) - **Credential vault** — six credential kinds in one place; plaintext is neither stored nor returned in lists, revealing it is a separate high-privilege action that lands in the audit trail, and expiry warns 7 days ahead. ![alt text](docs/screenshots/credential-vault.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 filtered by project language, scan rule inventory, quality metrics, red-line rules, promotion approvals; a red line can be a single threshold or a confidence expression folding several metrics into one score. ![alt text](docs/screenshots/artifact-report.png) ![alt text](docs/screenshots/quality-redline.png) - **Promotion queue** — deploys stopped by a "requires promotion" gate wait here: pending / approved / rejected tabs, how long they have waited, how many times they were blocked, and each row expands into its evidence (change scope and scan verdicts assembled on demand). What gets approved is the **artifact**, not a run. ![alt text](docs/screenshots/promotion-queue.png) - **AI task ledger** — one requirement's journey to shipping: a delivery funnel (filed → shipped → PR merged, three different denominators), token spend per model, and the three ticket kinds (requirement / self-heal / review); "trigger fix" is a human click, AI never rewrites code on its own. ![alt text](docs/screenshots/ai-tasks.png) - **Repo wikis** — repo understanding persisted per commit: overall summary, module map, build & test commands (only those that really exist in manifest files — otherwise it says "unrecognised" instead of guessing); an unchanged commit costs zero tokens. ![alt text](docs/screenshots/repo-wikis.png) - **Enterprise norms** — discipline written for the model: `guidance` goes into the prompt, `denyPatterns` are machine-enforced to refuse shipping; each norm carries a priority and a sharing scope (all tenants / listed tenants / one tenant). ![alt text](docs/screenshots/ai-norms.png) - **Marketplace** — plugin packages, versions, per-user installs, comments, the built-in plugin development guide, Kotlin/Shell templates and the `plg` scaffold. ![alt text](docs/screenshots/plugin-marketplace.png) - **Tenants** — the tenant ledger: members, pipelines, this month's usage against its cap, status (healthy / near budget / over budget / disabled), plus per-tenant execution quotas (concurrent runs, daily triggers, build-agent slots, shared build-agent minutes), a "claim ownerless pipelines" flow and a usage export. ![alt text](docs/screenshots/tenants.png) - **Message centre** — failure, approval and ticket events are stored once per recipient with independent read state; filter by unread, and the header bell only ever shows your own count. ![alt text](docs/screenshots/messages.png) - **Settings** — users & roles, notification channels, API tokens, audit log, AI provider, account. ![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 five roles enforced on the server (`SUPER_ADMIN` for platform governance, `TENANT_ADMIN` for one tenant, plus `OPERATOR` / `DEVELOPER` / `VIEWER`): reads are open to any signed-in user, **every write defaults to super admin / tenant admin / operator / developer** (a bare `@RequireAuth` still keeps a VIEWER out), and the two gates are distinct: `@PlatformOnly` admits only a `SUPER_ADMIN` who belongs to **no** tenant and guards the platform itself (tenant ledger and memberships, API token issuance, AI providers, marketplace registry sync and catalogue writes, reverse-proxy routes, schedule refresh, cross-tenant user operations); `@AdminOnly` is the business tier below it (super admin + tenant admin: revealing credential plaintext, red lines, notification channels, `docker system prune`). Login side: TOTP (with recovery codes) and WebAuthn as second factors, a session list you can prune other sessions from (with device and region from the offline `ip2region` DB), e-mail verification for unrecognised sources plus anomaly alerts — user-level switches where alerts default on and verification defaults off, because "no mail → cannot log in" has to be a deliberate choice rather than an upgrade side effect. 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 engine (24 source rules across security / defects / style / complexity, plus 17 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; a rule can also carry a confidence expression that folds several metrics into one score instead of a single threshold. Reports, per-file drill-down and a JSON export live on the run. - **⏱ Three things scans used to hide** — ① **where the time went**: per-engine elapsed time and the total (`engineTimings`) are written into the report and shown in the run's source breakdown, so "which tool was slow" is answerable instead of collapsing into one opaque duration. ② **truncation**: the built-in engine scans at most 2000 files per run; it used to stop silently at 2000, producing a report indistinguishable from a full-repo pass. It now sorts by path, takes the first 2000, and says so in the step log ("N files in total, only the first 2000 were scanned, X remain unscanned — this is not 'the repo is clean'"), with the in-process Kotlin path and the shell path selecting the same files. ③ **change attribution**: the report gains a `changeSet` (baseline commit → HEAD, changed files, files with issues, whether it was capped), every issue is tagged `changed`, and `summary.changed*` plus five **new** metrics (`CODE_SCAN_CHANGED_FILES/TOTAL/ERRORS/WARNINGS/INFOS`) count only findings inside the change set. Crucially it does **not** shrink the scan: red lines, trends, LOC and the full-repo verdict are unchanged — attribution is a second dimension layered on top, so a full-repo `<= 0` red line cannot turn green without a fix. A first run, a non-git workspace or an unreachable baseline yields an explicit reason rather than "0 new issues"; `incremental=false` on the node turns attribution off (that is not "incremental scanning", and the log says so). - **🧪 Dependency and licence scanning** — `dependency_scan` sits beside `code_scan`: it scans dependency lockfiles rather than the sources, dispatching `npm audit` / `pip-audit` / `govulncheck` / OWASP dependency-check / `cargo audit` by ecosystem, plus Go · Gradle · npm · pip licence scanners. Findings are normalised into a CVE list (report type `dependency-scan`) and `DEP_SCAN_TOTAL/CRITICAL/HIGH/MEDIUM/LOW` become run variables for the gate; the `PATH → Docker → skip` chain is the same as `code_scan`. - **🧹 Scan noise reduction (AI optional)** — 200 findings are first clustered locally into 5–10 groups (by rule + file extension + message prefix, pure Java, no model call), then one representative per cluster is sent to the model to ask "is this cluster a false positive?" — N calls for N clusters, not N calls for N findings. The model only ranks and merges, it **never deletes** findings, and the raw list stays expandable. A cluster judged false-positive can be turned into a suppression rule (`tenant + rule + file pattern + message prefix`, idempotent and audited) in one click; suppressed findings stop appearing in reports, and platform-level suppressions apply to every tenant. - **🚀 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 the tenant admin (scoped to that pipeline's own tenant, not a platform-wide blast), and approval-type events (`gate_pending` / `gate_rejected` / `ticket_open`) reach tenant admins and operators 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, a pipeline can name extra recipients (`notifyRecipients`) which are filtered the same way, 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 software factory (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). Four first-class nodes: `ai_design` (a plan and task breakdown grounded in the **checked-out** repo), `ai_coding` (requirement → edit → guardrail → push and open a PR/MR), `ai_test` (three machine checks defuse "write a test that is always green": the existing suite must be green before it starts, it may only add files, and green must come from re-running the suite itself) and `ai_wiki` (repo wiki cached per commit — the same `source_commit` means zero tokens). They are real `JobHandler`s, so they inherit build-machine / container routing, the job time budget, retries and audit; on an instance without AI they fail loudly instead of succeeding as a no-op. `ai_coding` also runs a **machine-judged** secret red line before shipping: a hit stops it at "no commit, no push, no PR" — no exception makes it acceptable to write credentials into repo history. - **🔁 Tickets: self-heal and review loops (filing is automatic, starting work is a decision)** — `ai_tasks` holds three kinds in one ledger: human-filed requirements, self-heal tickets opened for failed runs (failure attribution + log tail + repro context) and review tickets opened when a PR asks for changes (they reuse the original branch, so a push updates the existing PR). Both are produced by pollers reading the fact tables rather than by hooks on the terminal hot path, and a closed or merged PR retires its untriggered review ticket. **Tickets are filed automatically; code is not changed automatically** — a human clicks "trigger fix" before `ai_coding` starts, pipelines without `selfHeal` never call the API, and a run triggered by `ai_task` never files another ticket (no self-amplification). A policy layer (`ai_norms`, enterprise norms) feeds `guidance` into the prompt and lets `denyPatterns` be machine-rejected at ship time, with every change audited. - **🧠 The rest of the AI surface** — pipeline generation from a repo, run diagnosis, fix suggestions, and the ops assistant (natural language → shell, 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. - **🏷 Multi-tenancy and resource grants** — a tenant ledger with memberships (super admin / tenant admin / operator / developer / viewer), per-tenant execution quotas (concurrent runs, daily runs, probe slots on one build agent, shared build-agent minutes per day), and platform assets that can be **granted to named tenants** instead of the old all-or-nothing switch (`share_mode = all | listed` plus a grant list carrying grantor, expiry and revocation history, across servers / build agents / probes / credentials / repos / custom nodes / enterprise norms). Three layers decide: **ownership** (`tenant_id`) → **visibility** (`public` / `private`, where private means owner + super admin + the tenant admin of the same tenant) → **grant list**. What the console hides, the runtime also refuses to reference (a run is judged by the `tenant_id` snapshot on its own row, and fails closed when it cannot tell). - **🧹 Housekeeping & platform** — MySQL 8 + Flyway with MyBatis-Plus, run status changes and cron fires persisted as an event stream (`pipeline_run_events` / `cron_trigger_log`), run retention sweeper (default 90 days), two-layer crash recovery (`StaleRunRecoveryService` finalizes orphaned `running` / `queued` / `canceling` runs on boot and records the last event as the reason, 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` | 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 `