Skip to content

ci: real-Plane CE round-trip job (PFB-28)#16

Merged
hstern merged 2 commits into
mainfrom
feat/pfb-28-real-plane-e2e
May 24, 2026
Merged

ci: real-Plane CE round-trip job (PFB-28)#16
hstern merged 2 commits into
mainfrom
feat/pfb-28-real-plane-e2e

Conversation

@hstern

@hstern hstern commented May 24, 2026

Copy link
Copy Markdown
Owner

Summary

  • Adds an e2e-real-plane CI job that brings up a minimum Plane CE v1.3.1 stack (api+worker+postgres+redis+rabbitmq+minio, no frontends, no persistent volumes), seeds an admin user / workspace / project / API token via direct Django ORM, and round-trips the bridge's plane.Client against real Plane over the REST API. Gates publish alongside lint and e2e-docker.
  • Lives entirely under test/e2e-docker/plane-ce/ — compose file, seed script, runner shell script, README. No changes to existing e2e harness.
  • Integration test is build-tagged (//go:build integration) so default go test ./... is unchanged. Local run: bash test/e2e-docker/plane-ce/run.sh up && go test -tags=integration ./internal/plane.

Why

PFB-22, PFB-24, and PFB-25 were all the same class of bug: bridge testdata diverged from real Plane wire shape, the existing plane-stub happily agreed with the synthetic data, and the bug only surfaced when real Plane talked to the deployed bridge. We've whack-a-mole'd three of these in production. This is the structural fix — any future Plane wire-shape change fails CI on the next PR instead of failing production on the next deploy.

What the integration test pins

Every plane.Client method the bridge calls today: ListProjectStates, CreateProjectLabel, ListProjectLabels, ListWorkspaceMembers, CreateIssue, GetIssue, UpdateIssue, GetIssueByExternalRef, CreateComment, UpdateComment, DeleteComment. The PFB-25 failure mode (REST state field as a bare UUID, not the webhook's object form) is specifically asserted at every issue create/get/update point.

Why direct ORM (seed.py) instead of REST

Plane CE has no headless signup endpoint. The browser flow goes through Django session cookies + CSRF tokens + workspace onboarding, all of which are expensive to drive from a CI script and exist for human operators. seed.py runs inside the plane-api container and creates the rows directly via ORM — Instance (is_setup_done=True), User, InstanceAdmin, Workspace, Project, default State rows, APIToken. Every step is get_or_create; the seed is safe to re-run.

Cost

~90s of cold-cache time per CI run. The job is single-leg (not matrix) because Plane wire shape doesn't vary by forge flavor — running it once is enough. No persistent volumes; data dies with docker compose down.

Local verification (this PR)

bash test/e2e-docker/plane-ce/run.sh up
# Seed JSON on last stdout line; capture it:
eval "$(bash test/e2e-docker/plane-ce/run.sh seed 2>/dev/null | tail -n1 \
  | jq -r 'to_entries[] | "export PFB_PLANE_TEST_\(.key|ascii_upcase)=\(.value)"')"
export PFB_PLANE_TEST_BASE_URL=http://localhost:8765/api/v1
go test -tags=integration ./internal/plane -run TestIntegration -v
# PASS  TestIntegration_RoundTrip (1.06s)
bash test/e2e-docker/plane-ce/run.sh down

Trade-offs / footguns

  • Pinned to v1.3.1. README documents the bump procedure (recapture testdata, verify ORM surface, verify management commands). Drifting these together avoids the wire-shape coverage going stale.
  • Podman wart on rabbitmq: rootless podman runs the container as a UID that lacks read access to in-image /var/lib/rabbitmq/; compose pins user: "100:101" to work around it. Docker (CI) is unaffected.
  • Seed reaches into Plane internals (plane.db.models, plane.license.models). If Plane refactors those module paths between versions, the seed breaks. The breakage is loud (ImportError) and the README points at the spots to inspect.

Test plan

  • make race — all packages green
  • make lint — 0 issues
  • Local verification: full run.sh upgo test -tags=integration round-trip passes on this branch (Plane CE v1.3.1, fedora/podman)
  • CI: confirm the e2e-real-plane job lands green on this PR (the real signal — first time it runs against ubuntu-latest/docker rather than my local podman)

🤖 Generated with Claude Code

hstern and others added 2 commits May 24, 2026 05:44
PFB-22, PFB-24, and PFB-25 were all the same class of bug: the existing
test/e2e-docker harness uses a plane-stub that happily agrees with the
bridge's synthetic testdata, so any divergence between testdata and
real Plane wire shape only surfaces in production.

This adds an `e2e-real-plane` CI job that:

- Brings up a minimum Plane CE v1.3.1 stack via docker compose
  (api+worker+postgres+redis+rabbitmq+minio, no frontends, no
  persistent volumes). Trimmed from upstream makeplane/plane compose.
- Seeds an admin user + workspace + project + API token via direct
  Django ORM (seed.py runs inside the plane-api container).
  Plane CE has no headless signup flow — the browser flow goes through
  Django sessions + CSRF + workspace onboarding, all expensive to
  drive from a script. Direct ORM is idempotent and self-contained.
- Runs the bridge's plane package integration test against real Plane
  over REST. The test pins every plane.Client method the bridge uses,
  asserting the high-value fields decode (CreateIssue/GetIssue/
  UpdateIssue cover the PFB-25 state-as-bare-UUID failure mode).
- Gates publish alongside lint + e2e-docker.

Local: bash test/e2e-docker/plane-ce/run.sh up; go test -tags=integration ...

The job adds ~90s of cold-cache time per CI run. Acceptable cost for
the structural guarantee against future PFB-22/24/25-class bugs.

Refs PFB-28.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…library

Restructures the job per review: drop the library integration test
(redundant with `go test ./...` in lint), and instead pull the bridge
image produced by build-image, run it as a sibling container on the
plane-ce compose network, POST a synthetic Forgejo issues.opened
webhook at it, and assert via REST that real Plane has the work item
(looked up by external_source/external_id). The publish gate now
exercises the actual artifact that's about to ship.

Healthcheck fixes for the plane-api service from the first failed CI
run:

- curl is not in the makeplane/plane-backend image; switch to wget
  --spider (the image has wget).
- localhost inside the container resolves to ::1 first on some
  rootless podman setups, but gunicorn binds 0.0.0.0:8000 (IPv4 only),
  so the healthcheck got ECONNREFUSED. Use 127.0.0.1 explicitly.
- Add start_period: 60s so the cold-cache gunicorn boot + static
  collectstatic + bucket creation don't trip the healthcheck before
  the API is ready to serve.

README documents the new flow + the healthcheck rationale + the
rootless-podman file-mount quirk that affects local dev (but not CI).

Refs PFB-28.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@hstern hstern merged commit 30c0e2f into main May 24, 2026
6 checks passed
@hstern hstern deleted the feat/pfb-28-real-plane-e2e branch May 24, 2026 09:35
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