Skip to content

plane 1.0.0 — open-source project management - #529

Open
jacobecox wants to merge 4 commits into
mainfrom
claude/plane
Open

plane 1.0.0 — open-source project management#529
jacobecox wants to merge 4 commits into
mainfrom
claude/plane

Conversation

@jacobecox

Copy link
Copy Markdown
Contributor

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 OOMKilled restarts over 5.5 hours at worker.resources.maxMemory: 1Gi. Upstream's entrypoint runs celery with 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_concurrency only from Django settings, so the flag is the only lever — no env var exists), adding the worker.concurrency knob the spec had deferred, and sizing memory to the pool. Verified: banner reads concurrency: 4 (prefork), exactly 5 processes, zero restarts, 376Mi by cgroup at idle.

2. storage.type: minio — the default — could not upload or serve any attachment

Caddy must pass the client's Host through unchanged, because SigV4 signs it. The mesh routes by Host only on an http port, so every /uploads request became a mesh 503 — including on the public canonical endpoint, which ruled out a port-forward artefact.

Fixed with protocol: tcp on the MinIO container port: L4 passthrough, Host never inspected, both signature types validate. Controls run before touching the chart — arbitrary Host returns 200 on tcp and 503 on http, direct-to-MinIO returns a genuine 403 AccessDenied, and internalAccess still refuses a non-listed workload on tcp, so the firewall is unaffected.

Rewriting Host at 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 default storage.type stays.

3. The only documented backup path was broken in both directions

cpln workload exec decodes binary stdout as UTF-8 and substitutes U+FFFD — a 661,277-byte dump came back 692,603 bytes with 0xb4ef bf bd, and pg_restore -l segfaulted. 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 location the GVC lacks, the outcome is safe (maxScale: 0 confines 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, matching documenso.

Also proved here

$(VAR) expansion over cpln:// references works. Read inside the container, AMQP_URL, REDIS_URL and DATABASE_URL all 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-stored is 0 across all 22 resources this chart authors, redis/mq/minio included. The 9 remaining differences are inside the vendored postgres 3.4.1 (a partial defaultOptions block) and are not this chart's to fix.

Known limits

postgres cannot 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

jacobecox and others added 4 commits August 31, 2026 09:53
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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant