Thanks for your interest in improving zm-web! This guide covers how to get a change merged.
zm-web is dual-licensed β open source under AGPL-3.0, and available under a separate commercial license. For the project to be offered under both, every contribution must be covered by the Contributor License Agreement.
By opening a pull request you agree to the CLA for that and all future contributions. You keep the copyright in your work β the CLA is a license grant, not an assignment, that lets the maintainer license the project under both AGPL and commercial terms. Please read it before contributing.
The CLA Assistant workflow checks this automatically: if you haven't signed, a bot
comments on your PR and the CLA Assistant status check fails. Reply on the PR with:
I have read the CLA Document and I hereby sign the CLA
Your signature is then recorded in signatures/version1/cla.json (on the cla-signatures
branch) and the check passes. You only sign once β future PRs are recognised automatically.
The CLA check only blocks a merge if it is a required status check. After the
workflow has run at least once, configure it under
Settings β Branches β branch protection rule for main:
- Enable Require status checks to pass before merging and add
CLA Assistant. - Settings β Actions β General β Workflow permissions must be Read and write so the
action can commit signatures to the
cla-signaturesbranch (already configured).
Without the required-check setting the CLA status is advisory only. Note: requiring a status check (branch protection / rulesets) on a private repo needs GitHub Pro β it becomes available for free once the repo is public. Until then the bot still comments and records signatures; it just can't block the merge button.
Get set up with the Quick Start (Node 20+, npm install, and a
.env pointing VITE_API_PROXY_TARGET at a running zm-api
backend).
Before opening a PR:
-
Add or update a test that captures the behaviour change β colocated
*.test.ts(x)for unit logic (Vitest + Testing Library), or*.spec.tsfor end-to-end flows (Playwright). -
Implement the smallest change that makes it pass.
-
Run the quality gates β all must be green:
npm run lint # ESLint npm test # Vitest unit suite npm run build # tsc -b type-check + production build
-
If you touched a user-facing flow, run the end-to-end suite too (needs the dev server and a reachable backend):
npm run test:e2e # Playwright (:chromium / :webkit for a single browser)
- Dual-skin first. Every feature must work in both the Modern and
Classic skins. Put shared data/logic in
src/features/<feature>/(skin-agnostic hooks); route bodies dispatch onuseUiStore.skinand render either the modern panel layout or the classic table. SeeCLAUDE.mdfor the architecture. - Data fetching goes through TanStack Query β no raw
fetchin components. Add typed API wrappers insrc/api/<feature>.ts. - Styling is Tailwind classes only β no CSS modules or styled-components.
- Use the
@/path alias for imports fromsrc/. Components arePascalCase; routes are kebab-case or$paramfiles. - Keep changes small and focused; don't reformat or "clean up" unrelated code.
- Don't commit secrets.
.envis gitignored β never hard-code backend hosts or credentials (useVITE_API_PROXY_TARGET). See.env.example. - Note: adding routes requires the route tree to regenerate (run the dev server).
Open a GitHub issue with steps to reproduce, the expected vs actual behaviour, and relevant logs, console output, or screenshots (with any footage/credentials redacted).
- I agree to the CLA.
- Tests added/updated and passing.
-
npm run lint,npm test, andnpm run buildall pass. - The change works in both skins (Modern and Classic) where applicable.
- The change is focused and documented where it isn't self-evident.
main requires five checks to pass β typecheck Β· unit tests Β· build,
lint, e2e (seeded), container image and cla β so a red or unfinished
PR cannot be merged. gh pr merge <n> --auto --merge queues the merge behind
them; do that rather than watching and clicking.