Status: v0.1.2 — multi-arch image (linux/amd64 + linux/arm64) at
ghcr.io/hstern/plane-forge-bridge:0.1.2 (also :0.1, :0, :latest).
Patch release fixing two real-deployment bugs in the plane → forge path
that v0.1.0 shipped silently broken — see CHANGELOG.
Bidirectional webhook bridge between Plane and any Gitea-API-compatible git forge (Forgejo, Gitea). Closes parity with Plane's first-party GitHub/GitLab integration for the parts most operators need: issue / comment / label / state sync, plus PR-driven work-item state automation.
Plane's first-party GitHub and GitLab connectors live in a closed-source
service called silo (commercial, distributed via
artifacts.plane.so/makeplane/silo-commercial). The OSS makeplane/plane
repository contains no connector code, so a PR adding native Forgejo support
upstream is not actually possible —
makeplane/plane#1495
has been open since 2023 with no maintainer response. This repo is the OSS
satellite that fills that gap: an MIT-licensed alternative to the existing
paid third-party plugin discussed in
makeplane discussion #8796.
- Single static Go binary, stateless, one process. Distroless runtime.
- Two HTTP webhook endpoints (
/forge/webhook,/plane/webhook) with HMAC verification at the handler boundary, plus outbound REST clients to both sides. - Forgejo and Gitea share the v1 REST API and webhook payload format
(
X-Gitea-SignatureHMAC-SHA256,X-Gitea-Event) — one internalforgepackage speaks both. CI tests against both as a matrix on every PR. - Loop-break marker on every outbound write:
<!-- pfb:src=<forge|plane>,evt=<event-id> -->. Inbound events carrying our marker are dropped. Belt-and-braces: an in-memory LRU of recently-seen(source_event_id, target_object_id)pairs. - Identity mapping is a config map first, email-based match second (works against Gitea/Forgejo's webhook noreply placeholder by re-resolving via the user-search API), bridge bot as the fallback.
See docs/design.md for the full design + open questions.
-
Pull the image
docker pull ghcr.io/hstern/plane-forge-bridge:0.1.2
-
Write a
config.yaml— copyconfig.example.yamland fill in the placeholders. The config names environment variables that hold the secrets (it does not store the secrets themselves), so the file is safe to commit alongside infrastructure code. -
Run it
docker run -d --name pfb \ -p 8080:8080 \ -v $PWD/config.yaml:/etc/pfb/config.yaml:ro \ -e PFB_FORGE_TOKEN=... \ -e PFB_FORGE_WEBHOOK_SECRET=... \ -e PFB_PLANE_API_KEY=... \ -e PFB_PLANE_WEBHOOK_SECRET=... \ ghcr.io/hstern/plane-forge-bridge:0.1.2 \ --config /etc/pfb/config.yaml -
Register the webhook on the forge side:
POSTto/api/v1/repos/<owner>/<repo>/hookswithtype: "gitea",url: https://<bridge-host>/forge/webhook, the same HMAC secret asPFB_FORGE_WEBHOOK_SECRET, and the events you want (issues,issue_comment,pull_request,pull_request_review). -
Register the webhook on the Plane side: workspace settings → Webhooks → add
https://<bridge-host>/plane/webhookwith the same HMAC secret asPFB_PLANE_WEBHOOK_SECRETand the relevant event types. -
Tail the logs:
docker logs -f pfb. JSON structured (log/slog); each accepted webhook is one info-level line withkind,delivery_id, and the resulting outbound action.
Bidirectional
- Issues create/update/close/reopen ↔ work items (title, description, state, assignee, labels)
- Comments create both ways (edit/delete deferred)
- Labels both ways with auto-create on either side
- State mapping per-link (forge
open/closed↔ configurable plane states)
One-way (forge → plane)
- PR open/reopen/close/merge → state automation on the linked work item via
[PROJ-123]bracket refs in title/body orproj-123-foohead-branch names (review-state semantics deferred) - Plane → forge issue creation closes the loop (work_item.created → forge issue created with both loop-break and plane-ref markers; plane updates + deletes deferred)
Identity
- Static
forge_username → plane_member_uuidconfig map (operator override) - Email-based matching as fallback — defeats Gitea/Forgejo's webhook noreply placeholder by re-resolving through the user-search API
- Configurable bridge bot as the final fallback
Out of v0.1: comment edit/delete identity persistence, plane work_item update/delete reverse lookup, PR-review-state automation, plane→forge label sync wire-up, custom OAuth flow (only needed for multi-tenant heterogeneous-email deployments), label cache invalidation on rename, PR state no-op suppression, plane→forge body author preface.
make build # go build ./...
make test # go test ./...
make race # go test -race ./...
make lint # golangci-lint run ./...
make vuln # govulncheck ./...
make image # docker buildx build
make e2e # e2e against Forgejo + Gitea + plane-stub containersCI runs all of these on every PR; the matrix e2e brings up real Forgejo 15 and Gitea 1.22 service containers, drives them via REST, and asserts on plane-stub's recorded calls.
MIT.