Contributing
TruePPM is in its early days and contributions are welcome. The project uses GitLab for issue tracking and merge requests.
Before you start
Section titled “Before you start”Looking for something to work on? Browse issues carrying the
backlog label —
triaged and not yet scheduled. Check that an issue doesn’t already have an
open merge request or an assignee before you pick it up.
For anything beyond a typo or one-line fix, open an issue first describing what you want to change and why. This avoids wasted effort if the change conflicts with the roadmap or the OSS/Enterprise boundary below.
Who reviews, and how fast: the maintainer monitors the issue tracker and merge requests, and responds within a few business days.
Community channel: GitLab issues and merge requests are the project’s only channel today — there is no separate chat or forum yet.
Code of conduct: participation is governed by the Contributor Covenant 2.1. Be respectful — disagreement about technical choices is fine, disrespect toward contributors is not.
Governance: who maintains TruePPM, how decisions are made and recorded, whether maintainership is open, and how the open-core boundary is decided are set out in GOVERNANCE.md.
Getting set up
Section titled “Getting set up”git clone git@gitlab.com:trueppm/trueppm.gitcd trueppmmake setup # installs git hooks via pre-commitmake doctor # verifies all prerequisitesSee Installation for Docker Compose setup.
Frontend environment variables
Section titled “Frontend environment variables”The web app reads build-time settings from packages/web/.env (gitignored) — copy
packages/web/.env.example and adjust. Vite only exposes variables prefixed with
VITE_, and a change takes effect only after restarting the dev server.
| Variable | Default | Purpose |
|---|---|---|
VITE_API_BASE_URL | unset (dev proxies to localhost:8000) | Deployed API base URL for production builds |
VITE_FEATURE_FLAGS | unset | JSON build-time defaults for runtime feature flags |
VITE_REACT_QUERY_DEVTOOLS | off | Set to true to show the React Query devtools panel in dev builds |
The React Query devtools panel is off by default so it never occupies screen
real estate during normal development. To debug query-cache state, set
VITE_REACT_QUERY_DEVTOOLS=true in packages/web/.env and restart Vite. It is
gated on dev builds, so it is never present in a production bundle regardless of
this value.
Branching
Section titled “Branching”Branch from main with a conventional prefix:
git checkout main && git pull origin maingit checkout -b feat/my-feature # or fix/, docs/, chore/, test/, refactor/Never commit or push directly to main — all changes go through a feature branch and merge request.
Commits
Section titled “Commits”Follow Conventional Commits:
feat(web): add board/kanban viewfix(api): prevent duplicate membership creationdocs(scheduler): add Monte Carlo CLI examplestest(api): add permission tests for task deletionchore(ci): upgrade Node to 22 in CI imageScopes: scheduler, api, web, helm, sync, docs, ci.
Changelog
Section titled “Changelog”Every MR that touches source code must include a changelog fragment in changelog.d/:
# Naming: <slug>.<type>.md# Types: added, changed, fixed, securityecho "Add board/kanban view with drag-and-drop" > changelog.d/kanban-view.added.mdThe CI changelog:check job blocks the pipeline if the fragment is missing. Fragments are assembled automatically at release time — never edit CHANGELOG.md directly.
Exempt: CI config, dependency bumps, test-only changes, docs-only changes.
Testing
Section titled “Testing”make test # runs all packages# Or per-package:cd packages/scheduler && pytestcd packages/api && pytestcd packages/web && npm test- Scheduler: pytest, coverage >= 80%
- API: pytest with testcontainers PostgreSQL, coverage >= 80%
- Web: vitest, coverage >= 75%
The API suite bans real outbound network sockets: a test that reaches the live
network (usually a misdirected mock) fails fast with a SocketConnectBlockedError
instead of hanging on a connect timeout and flaking. Only the configured database
and Redis hosts are allowed. A test that genuinely needs the network must opt out
explicitly with @pytest.mark.enable_socket — keeping the exception visible and
reviewable.
All MRs require a green pipeline before merge.
Run make pre-push before every git push — it mirrors the blocking CI gates (lint, typecheck, migrations-check, schema-check).
Code style
Section titled “Code style”| Package | Formatter | Linter | Type checker |
|---|---|---|---|
| Scheduler | ruff format | ruff check | mypy |
| API | ruff format | ruff check | mypy —strict |
| Web | prettier | eslint | tsc —noEmit |
make lint # runs all lintersmake typecheck # runs all type checkersDocumentation screenshots
Section titled “Documentation screenshots”The product screenshots in the docs (packages/website/src/assets/screenshots/)
are captured from a running dev stack loaded with the Atlas Platform Launch
sample, not edited by hand. When a UI change makes one stale, re-capture it:
make updocker compose exec api python manage.py load_sample_project --with-personascd packages/websitenpm run screenshots # every shotnpm run screenshots -- --only schedule,boardnpm run screenshots -- --list # names and routesThe script (scripts/capture-screenshots.mjs) signs in through the API as the
demo PM persona (atlas-alex, password demo under DEBUG) and a contributor
(atlas-tom) for the personal surfaces, dismisses first-run prompts, and writes
WebP files at a fixed 1600×1000 viewport. Add a new surface by appending an entry
to its shot list; the ready selector should be something that only renders
after the page’s data loads, not the chrome. Reference an image from a page as
.
Merge requests
Section titled “Merge requests”- Push your branch and open an MR targeting
main - Wait for a green pipeline
- Include: description, testing done, screenshots (if UI), issue link
- Don’t merge with a failing pipeline — fix the root cause on the branch
OSS / Enterprise boundary
Section titled “OSS / Enterprise boundary”Before writing code for a new feature, determine if it belongs in the community or enterprise repo:
- Community (this repo): everything a PM or program team needs to run a program (including multi-project programs)
- Enterprise (separate repo): cross-program/portfolio governance, compliance, and org-level policy
The community edition must never import from trueppm_enterprise. Verify with:
make enterprise-boundary-check # OK: no trueppm-enterprise imports in packagesA plain grep -r "trueppm_enterprise" packages/ is not the check. The tree legitimately names the package in extension-point docstrings and ADR pointers — that grep returns 12 lines across 8 files on a clean tree. The gate matches import syntax and quoted module paths, and ignores comments (#2603). The same check runs in CI as boundary:imports.
WebSocket events must have a consumer on both ends
Section titled “WebSocket events must have a consumer on both ends”Adding a broadcast_board_event() call is half a feature. If nothing on the client registers a handler for the event type, the broadcast reaches the browser and is discarded, and the surface it was meant to refresh stays stale until something else invalidates it. Three separate audits each found a different slice of exactly that, so the reconciliation is now a gate:
make ws-handler-conformance-check # same as the lint:ws-handler-conformance CI jobIt cross-references every event type reaching broadcast_board_event() / abroadcast_board_event() in packages/api — through the same AST sweep that enforces the frozen event set, including one level of wrapper indirection — against the on(...) registration table in packages/web/src/hooks/useProjectWebSocket.ts. It runs in both directions and also fails on a duplicate registration, because on() is last-write-wins: a second on('task_updated', …) silently replaces the first rather than adding to it.
When you add an event, add its handler in the same MR. When the omission is deliberate — or the event is structurally undeliverable, as the program_* events are until the program channel ships — record it in packages/api/tests/apps/sync/ws_handler_waivers.py with a reason and the issue number that removes it, and raise that ledger’s budget by one in the same diff. The budget is what makes an addition visible in review; the gate’s own staleness checks force an entry out again once the handler lands.
The job is deliberately not change-gated. Deleting a handler is a web-only diff, which the API test suite never runs on.