plane 1.0.0 — open-source project management - #529
Open
jacobecox wants to merge 4 commits into
Open
Conversation
Plane Community Edition 1.4.2 (AGPL-3.0) — open-source project and issue tracker. Fills the catalog's empty project-tracking category. 7 workloads: one six-container single-origin HTTP workload (proxy + web + space + admin + live + api on loopback), a Celery worker, a Celery scheduler that also runs migrations, Valkey, RabbitMQ, bundled MinIO, and the postgres subchart pinned to postgres:16. No GVC is created; everything is pinned to one location with defaultOptions scale 0 plus a boot-time GVC read. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…on claim, and an unvalidated triple-load-bearing value Five review warnings and two nits, none of them blockers, all of them things a user would have hit before we did. 1. The restore's first instruction could not be carried out. It said to "stop the api, worker and beat workloads first", and there is no way to: worker replicas below 1 are refused at render, beat has no replicas knob at all, and localOptions[].suspend is the workload-poisoning path CLAUDE.md forbids. The parenthetical is gone. The restore now runs against a live Plane and force-redeploys the three backend workloads afterwards so they reconnect — a command that exists and works — with the log noise during --clean stated plainly instead of implied away. 2. The dump/restore transport is unproven, so it now says so. The recipe needs binary stdout streaming out of `cpln workload exec` and local stdin forwarding into it, and no README in this catalog documents either. The round-trip was exercised locally against a postgres:16 container; the transport was not. That is spec test row T22. The README says it is unverified and tells the user to check their own dump with `pg_restore -l` before relying on it; the briefing no longer reads as though the cpln channel had been exercised. This is the only backup path the chart has — deleting it or inventing a replacement would both be worse. 3. "Extra GVC locations are fine — nothing Plane-related runs in them" was not true. Every workload this chart owns is pinned, but the bundled postgres subchart is not (a parent cannot template subchart placement), so an extra location does start a second Postgres with its own empty volume. The briefing already said this correctly; the README and the values comment now match it. The gap itself is maintainer-accepted — the claim was the defect. 4. Added the first-upgrade datastore re-apply note. It was missing from Important Notes even though this template's own First run flow (install private, upgrade to publish) walks every user straight into that upgrade, with four single-replica datastores behind it. 5. plane.appUrl is triple load-bearing and was unvalidated. A scheme-less value silently breaks three things at once: MINIO_ENDPOINT_SSL drops to "0" and attachments are mixed-content-blocked, CORS_ALLOWED_ORIGINS (which IS CSRF_TRUSTED_ORIGINS) gets an origin Django will not trust so every POST fails, and invite mail links have no base URL. Added a render-time fail requiring http:// or https://, and put the CORS==CSRF finding — previously only in the briefing — into the README where a user can act on it. Nits: plane.api.gunicornWorkers used `not (int ...)`, which let a negative value through; it now matches the three `lt ... 1` checks beside it. And the MINIO_ENDPOINT_SSL widening (publicAccess OR an https appUrl, where spec D9 says publicAccess alone) is recorded in the briefing — it is deliberate and safer, but it was an undeclared deviation. Verified: default render clean with only global.cpln.gvc; both arms of the new appUrl guard (scheme-less fails with the message, https:// and http:// render); gunicornWorkers -1 and 0 now fail, 2 renders; template-lint clean; every README YAML block still matches values.yaml. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The reviewer judged both correct, but neither existed anywhere on disk -- they were declared only in the builder's report, which is not an artifact that survives. The briefing is, so they go there beside the D9 one. The $(VAR) expansion is worth naming explicitly: it is the workaround for the render-time-string trap CLAUDE.md documents as a hard constraint, and langfuse has shipped it across six versions.
Test round 1 returned 23 PASS / 4 FAIL / 2 PARTIAL and judged the template not shippable. All four FAILs are fixed and verified on a live release (test-plane2 in test-gvc-3), not by inspection. 1. The Celery worker OOM-crash-looped on shipped defaults (65 OOMKilled in 5.5 h). Upstream's docker-entrypoint-worker.sh runs celery with no --concurrency, so Celery sized its prefork pool from the HOST's CPU count and started 16 Django children into a 1Gi limit. Spec item F4 deferred a concurrency knob arguing worker.replicas covered it; it does not, because replicas multiply pools rather than narrowing one. The worker start script now performs upstream's two waits itself and invokes celery with the pool width pinned. Adds worker.concurrency (default 4, guarded at render) and raises worker maxMemory to 2Gi. Celery reads worker_concurrency only from Django settings, so the flag is the only lever - there is no env var. Verified: Celery's own banner reports "concurrency: 4 (prefork)", exactly 5 processes, zero restarts, 767Mi summed RSS (376Mi by cgroup) at idle. 2. storage.type: minio - the DEFAULT - could not upload or serve any attachment. The mesh routes an `http` port by Host/:authority; Caddy must forward the browser's Host because the SigV4 signature covers it, so every /uploads request became a mesh 503. The fix is `protocol: tcp` on the MinIO container port, which makes the sidecar do L4 passthrough and stop inspecting Host. Rewriting Host at Caddy was the tempting wrong fix: it repairs uploads (a presigned POST signs the policy) and breaks every presigned GET. Isolated with both controls - an arbitrary Host returns 200 on a tcp port and 503 on an http port, and internalAccess still refuses a non-listed workload on the tcp port, so the firewall is unaffected. Verified end to end on the PUBLIC canonical endpoint (the path the round proved broken) and through a tunnel: upload 204, download 200 with bytes identical to the upload, object present in /export/uploads. 3. The only documented backup path was broken in both directions. cpln workload exec decodes binary stdout as UTF-8 and substitutes U+FFFD, and the restore omitted --stdin (default false), so its input was discarded. The README now dumps to a file inside the container, moves it out base64-encoded, and restores with --stdin. Run verbatim end to end: dump (676585 bytes, md5 matching in-container), DROP TABLE issues CASCADE, restore, all 24 rows and 164 migrations back, app serving. The old form reproduced the corruption exactly (+6.6%, 0xb4 -> ef bf bd at offset 130). Also corrected the archive check: a local pg_restore older than the server fails with "unsupported version (1.15)", so the README lists the archive in the container instead. 4. The beat boot check could not fire in the case it documented. It needs beat running, which needs the location to be valid - so on a fresh install with a wrong location beat is placed nowhere and logs zero lines. Stopped promising it in values.yaml, the README and the briefing, and documented the real diagnostic instead (every workload deactivated with "maxScale is set to 0", silent logs), matching documenso's candour. The check is kept and honestly scoped to the directions it can observe. Two smaller corrections the round asked for: plane.fileSizeLimit is enforced by Caddy and the storage policy, not the API; and the briefing's "attachments don't render through a port-forward" caveat is replaced with what defect 2 actually was. Gates: default render clean with only global.cpln.gvc; lint clean (6 pre-existing R5 WARNs, all standard workloads); 72 README scalars match values.yaml with zero mismatches and no undocumented keys; all 8 README URLs 200; render-vs-stored unchanged at 22 ok / 9 differences inside the vendored postgres 3.4.1 subchart, with minio still ok; no-op upgrades #2 and #3 fully Unchanged (23/23). Teardown verified by re-reading: 0 workloads, 0 volumesets, 0 identities, no org leftovers, GVC spec byte-identical. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Plane — open-source project management (issues, cycles, modules), with bundled PostgreSQL, Redis, RabbitMQ and MinIO behind a Caddy origin.
The largest template in this batch: 7 workloads, 23 resources, six services on one origin.
Testing
A full matrix, then a fix round with every fix verified live. First pass: 23 PASS · 4 FAIL, judged not shippable — three defects individually blocking, all three failing silently. All four are now closed and re-verified.
1. The Celery worker OOM-crash-looped on shipped defaults
65
OOMKilledrestarts over 5.5 hours atworker.resources.maxMemory: 1Gi. Upstream's entrypoint runscelerywith no--concurrency, so it sized its prefork pool from the host's 32 CPUs and started 16 Django children. On a default install all background processing was dead — and an OOM kill is invisible from inside the container, so nothing in the app logs said why.Fixed by pinning the pool (Celery reads
worker_concurrencyonly from Django settings, so the flag is the only lever — no env var exists), adding theworker.concurrencyknob the spec had deferred, and sizing memory to the pool. Verified: banner readsconcurrency: 4 (prefork), exactly 5 processes, zero restarts, 376Mi by cgroup at idle.2.
storage.type: minio— the default — could not upload or serve any attachmentCaddy must pass the client's
Hostthrough unchanged, because SigV4 signs it. The mesh routes byHostonly on anhttpport, so every/uploadsrequest became a mesh 503 — including on the public canonical endpoint, which ruled out a port-forward artefact.Fixed with
protocol: tcpon the MinIO container port: L4 passthrough,Hostnever inspected, both signature types validate. Controls run before touching the chart — arbitraryHostreturns 200 ontcpand 503 onhttp, direct-to-MinIO returns a genuine403 AccessDenied, andinternalAccessstill refuses a non-listed workload ontcp, so the firewall is unaffected.Rewriting
Hostat Caddy would have been the wrong fix — it repairs uploads (a presigned POST signs the policy, not the host) and breaks every presigned GET. Verified end to end on the public endpoint: upload 204, download 200, bytes identical, object present in/export/uploads. No trade-off was needed, so the defaultstorage.typestays.3. The only documented backup path was broken in both directions
cpln workload execdecodes binary stdout as UTF-8 and substitutes U+FFFD — a 661,277-byte dump came back 692,603 bytes with0xb4→ef bf bd, andpg_restore -lsegfaulted. Identical CR/LF counts ruled out line-ending translation. Separately the restore omitted--stdin(default false), so its input was silently discarded and the file landed at 0 bytes.The procedure now dumps in-container, moves the file out base64-encoded, and restores with
--stdin. Run verbatim end to end: dump →DROP TABLE issues CASCADE→ restore → 24 rows and 164 migrations back, app serving. The old form reproduced the corruption exactly.This is platform/CLI behaviour, not a Plane defect, and is now recorded in CLAUDE.md — it affects any template piping binary through exec.
4. A documented safety check that could not fire
With a
locationthe GVC lacks, the outcome is safe (maxScale: 0confines everything) but the beat boot check the docs promised never ran — beat is placed nowhere and logs zero lines. The branch is unreachable by construction. The docs now describe the real diagnostic instead of promising a check that cannot happen, matchingdocumenso.Also proved here
$(VAR)expansion overcpln://references works. Read inside the container,AMQP_URL,REDIS_URLandDATABASE_URLall arrive fully expanded with resolved secret values, and Celery genuinely consumed from a broker addressed that way. CLAUDE.md's render-time-string rule has been narrowed accordingly.render-vs-storedis 0 across all 22 resources this chart authors, redis/mq/minio included. The 9 remaining differences are inside the vendoredpostgres3.4.1 (a partialdefaultOptionsblock) and are not this chart's to fix.Known limits
postgrescannot be pinned by a parent, so an extra GVC location starts an idle database — stated plainly rather than claimed solved. Keyless S3 (T15) and live cross-replica editing (T18) remain untested and are named as such.🤖 Generated with Claude Code