Contributing
Thanks for wanting to help. This is the public platform repo. The live Collaborative Journalism Summit site stays in jamditis/cjs2026 and is not the place to send platform PRs.
By participating you agree to the Code of Conduct. Reports go to info@eventrunner.org.
Where to start#
Pick the path that matches what you are doing. If you are not changing code, you probably want Discussions or the wiki, not a pull request.
| You are… | Do this |
|---|---|
| An attendee who cannot sign in, find a session, or open materials | Q&A |
| Event staff stuck in the CMS, schedule, or speaker tools | Q&A |
| An organization that wants CCM to run your event | General or email info@eventrunner.org |
| Reporting a bug in the shared product | Bug form |
| Asking for a product change | Feature form |
| Sending code or docs | Open or link an issue, then a pull request |
If CCM is operating the event site for you and the problem is access, content, or your specific deployment, email info@eventrunner.org. That is support, not a public ticket.
Contribution policy#
We take:
- Bug fixes with a failing test when one can exist
- Features that match the roadmap or have a linked, accepted issue
- Docs, wiki corrections, and accessibility fixes
- Repro cases that do not include real attendee or client data
We will bounce:
- Hardcoded event names, cities, hex colors, or domains
- Personal infrastructure (hostnames, chat IDs, personal emails, private operator adapters)
- Secrets, live-project credentials, or real PII in tests, fixtures, screenshots, or logs
- A second queue.
ticket_sync_queueis the only queue in the system - Features already cut from v1 (video generator, bulk broadcast, social feeds, speaker chat) unless a linked issue reopens the case. Sponsor invoicing was reopened by #208.
- Drive-by refactors with no product change
The governance note is the short version of who decides.
Dev setup#
- Fork and clone this repository.
- Use Node 22, release 22.12 or later (
node -v); Vitest 5 needs it. The Cloud Functions runtime pins that major, sofunctions/package.jsondeclares"node": "22"and npm warns (EBADENGINE) on newer majors. - From the repo root:
npm run prepare:functions # packs packages/shared into functions/vendor/ — required before install
npm install
npm run lint
npm test
npm run test:rules # needs Java 21+ for the Firebase emulators
The Firebase CLI is firebase-tools 15, a root devDependency, so npx firebase runs the pinned version. Version 15 does not start the emulators on Java below 21. It is held at ~15.22.4. Later 15.x releases send emulator calls to 127.0.0.1 through HTTPS_PROXY and ignore NO_PROXY. Do not raise the range until a release fixes that; scripts/ci/audit-exceptions.json has the detail.
No .env and no cloud credentials are required for any of these — test:rules starts local Firestore and Storage emulators against a demo-* project id.
apps/web has its own vitest suite (npm run test -w apps/web) and its own dev loop against the Functions/Firestore/Auth emulators — see apps/web/README.md for the full commands, including the Playwright-driven sign-in smoke test (scripts/dev/login-smoke.mjs).
How to send a change#
- Search issues and the project board. The What this is column is the plain-language version of each title.
- Open an issue first if the change is more than a typo. Use the form that matches the work.
- Branch from
main. One logical change per branch. - Add or update tests next to the code (
*.test.cjsbeside the module). - Fill in the pull request template, including how you verified the change.
- Sign off each commit (
git commit -s) under the Developer Certificate of Origin.
Sign your commits (DCO)#
Every commit needs a Signed-off-by trailer:
git commit -s -m "Fix schedule timezone rollover"
That adds a line to the commit message — Signed-off-by: Your Name <you@example.org> — using the name and email from your git config. It is not a CLA and nobody countersigns it; by adding it you are personally certifying the Developer Certificate of Origin (spec §1.5): that you wrote the change or otherwise have the right to submit it under this project's license, and that you understand it is public and permanently recorded with your contribution.
Forgot on a commit that is not pushed yet: git commit --amend -s. Forgot on several: git rebase --signoff <base-branch> rewrites every commit on your branch to add the trailer, then force-push your branch (never main).
CI enforces this. The dco job in .github/workflows/ci.yml walks every non-merge commit in the pull request's range and fails if any is missing the trailer — node scripts/check-dco.cjs <base-sha> <head-sha>, credential-free, so it runs identically on a fork PR. A PR failing only the DCO check does not need new code: sign off the existing commits as above and push.
Shared package#
packages/shared is CommonJS first. Add a .mjs re-export shim next to every new .cjs module.
There is exactly one queue#
ticket_sync_queue is the only queue in this system, and it is not a precedent. It exists for one reason the code cannot work around: the ticketing provider's own read APIs are eventually consistent, so an order a webhook announces is often not readable yet. It is scoped to functions/src/ticketing/, capped at six attempts, and raises an operator alert when it gives up — nothing is ever dropped silently.
Everything else sends, writes, or fails in the request that asked for it. Email in particular has no queue: a managed provider owns retry and pacing (spec §3.1). A pull request that adds a second queue, a scheduled retry collection, a "pending work" document set, or a drain function needs an ADR first — say what makes the work impossible to do inline, and why a provider or a trigger cannot do it. See docs/adr/0001-event-platform-v1.md §3.3 and §10 question 9.
Scheduled reminders are the one timed sender, and ADR 0006 is why they are not a queue. Each run works out what is due from the schedule settings and the clock. Its ledger records only what was sent, and no run reads it to find work. It sends through the same paths as a staff send. A change that stores work for a later run, or retries a failed send in the background, needs a new ADR.
Accessibility#
UI changes need a keyboard path and visible focus. If you change a flow that attendees or staff use, say how you checked it (keyboard, zoom, or a screen reader). Do not rely on color alone.
The full interface bar — accessibility, typography, color tokens, motion, and microcopy — is docs/interface-guidelines.md. UI pull requests are reviewed against it.
Tests#
| Command | What it covers |
|---|---|
npm run lint | ESLint over every workspace, including the hex-literal ban (spec §7.6) and react-hooks/rules-of-hooks |
npm test | packages/shared and functions unit tests (Node --test, fakes — no emulator) |
npm run test:rules | Firestore and Storage security rules on the Firebase emulators |
npm run test -w apps/web | apps/web component/context/lib unit tests (vitest + Testing Library, jsdom — no emulator) |
npm run build -w apps/web | Production build of apps/web; credential-free with dummy VITE_FIREBASE_* values |
npm run test:e2e | Playwright end-to-end suite (OTP sign-in, CMS edit → publish → public, speaker invite → accept → wizard, ticket claim → approved → bookmark, browser installability of the default manifest and app icons) against the full Firebase emulator suite, seeded from the synthetic demo fixture — see e2e/ |
./gitleaks detect --source . | Secret scan (.gitleaks.toml) — CI downloads and checksum-verifies the gitleaks CLI directly rather than a marketplace action, so it stays credential-free on a fork PR (an org-repo license secret is not something a fork PR could ever have) |
node scripts/build-pages.cjs --check | The committed documentation site under docs/docs/ matches a fresh render of the source Markdown — see scripts/README.md |
node scripts/check-dco.cjs <base> <head> | Every non-merge commit in the pull request's range carries a DCO Signed-off-by trailer — see Sign your commits |
node scripts/dev/login-smoke.mjs | Live Playwright smoke test of the emailed-code sign-in flow against the Functions/Firestore/Auth emulators (dev tool, not run by CI) |
CI runs the trust checks and selected tiers on every pull request, credential-free. A manual CI run checks the full matrix. Fork PRs run on GitHub-hosted runners without credentials.
Proportional CI#
Pull requests run the trust checks (DCO and secret scanning) plus the smallest
path-selected tier. The repository-owned scripts/ci/classify-changes.cjs
classifier emits the selections; mixed changes take the union of their tiers.
A pull request runs that classifier from the base commit, so the pull request cannot replace the rules that select the security tiers.
The CI gate job is the stable aggregate check for branch protection. A job
that the classifier did not select is expected to be skipped. Merges do not
repeat the matrix that passed on the pull request. Deployment verification
still runs. All tiers remain credential-free.
Local CI on operator machines#
Run expensive checks on an operator machine before opening a PR. Use Node 22, Java 21, and Playwright Chromium with its sandbox enabled. The runner packs the shared package and installs the locked dependencies before testing. The emulator tests bind to localhost and use synthetic data. Run them in an isolated checkout with no production credentials. Keep concurrent emulator jobs on separate machines.
Commit the change and regenerate required output first. With a clean worktree that contains its base commit, run:
node scripts/ci/run-local.cjs --base <full-base-sha> --report /tmp/eventrunner-ci.json
The runner selects the same tiers as Actions, records each successful check, and refuses a receipt if the worktree changes during the run. Keep its output and receipt with the task record. Push the tested branch, then publish the receipt before creating the PR:
node scripts/ci/run-local.cjs --base <full-base-sha> --report /tmp/eventrunner-ci.json \
--publish --repo CenterCoopMedia/eventrunner
The repository variable LOCAL_CI_ACTOR_ID identifies the trusted status
publisher by its numeric GitHub user ID. For a same-repository PR, Actions
checks the newest status on the exact head, with the full base SHA in its
context. The result must come from that publisher and be less than 24 hours
old. The verifier is read from the PR's base. Missing, stale, conflicting, or
untrusted results use the hosted checks. Fork PRs always use hosted checks.
An updated base or head needs a new local run. A manual workflow run always
uses the hosted matrix. Superseded PR runs are canceled.
GitHub still runs DCO, secret scanning, and the required CI gate. Branch
protection and review requirements stay in force. The local publisher is a
trusted operator attestation; it is not the independent credential boundary
tracked in issue #259.
The documentation check validates local links and the Pages entrypoint's basic
metadata. Generated pages under docs/docs/** must also carry an SVG favicon,
canonical URL, and complete Open Graph and Twitter metadata. The built demo in
docs/demo/** remains outside that generated-page metadata rule until the demo
redesign in #109; its metadata is owned there. Root-page social metadata is
tracked in #105.
docs/demo/** is build output and is not committed. Do not rebuild or commit it
in a pull request. .github/workflows/pages.yml builds it with
scripts/build-demo.cjs on every push to main and deploys docs/ with it to
GitHub Pages. Links into docs/demo/ are checked only when a local build is
present; the Pages workflow checks them after its build.
docs/docs/** is generated output, not a place to edit. scripts/build-pages.cjs
renders it from the Markdown listed in scripts/lib/pages-manifest.cjs, and
scripts/build-pages.test.cjs fails the documentation tier when the committed
HTML has drifted from a fresh render — the same freshness rule
apps/web/src/generated/** lives under. Edit the Markdown, then run
node scripts/build-pages.cjs and commit both.
| Changed paths | Selected tier |
|---|---|
Markdown, docs/** except docs/demo/**, handbook files, and issue/discussion templates | Documentation checks: local links, generated Pages HTML markup, and docs/docs/** freshness |
Documentation generators (scripts/build-pages.cjs, scripts/build-pages.test.cjs, scripts/lib/pages-*.cjs, scripts/lib/markdown-pages.cjs) | Documentation checks and lint |
scripts/build-demo.cjs or scripts/build-demo.test.cjs | Lint, demo generator tests, and demo build hygiene: two identical builds and the demo bundle budget |
The Pages site address (scripts/lib/pages-site.cjs and its test) | Documentation checks, lint, and demo build hygiene |
apps/web/** | Lint, web unit tests, web build, generated-content hygiene, demo hygiene, and E2E |
functions/** | Lint, shared/functions unit tests, rules emulators, and E2E |
packages/shared/** | Lint, shared/functions unit tests, web unit tests, web build, generated-content hygiene, demo hygiene, rules emulators, and E2E |
firestore.rules or storage.rules | Rules emulators and E2E |
Workflows, tool configuration, package manifests or locks, Firebase configuration, e2e/**, publisher/**, other scripts, or an unrecognized path | Full matrix: documentation checks, lint, shared/functions unit tests, web unit tests, web build, generated-content hygiene, demo hygiene, rules emulators, and E2E |
Changes that match more than one row run the union of the selected tiers.
The main branch protection rule requires the stable CI gate check.
npm run test:e2e needs Java 21+ (the same Firestore/Storage emulators test:rules uses) and a Chromium build Playwright can find. It never runs playwright install itself: point PLAYWRIGHT_BROWSERS_PATH at an existing install, or run npx playwright install --with-deps chromium once yourself first — .github/workflows/ci.yml's e2e job does the latter, cached.
Releases#
Tags, CHANGELOG.md hygiene, and what a release includes are in RELEASING.md.
One lint rule to know about before it bites you: hex color literals (#336699) are banned everywhere except functions/src/email/templates/**, functions/src/schedule/pdf.cjs, functions/src/pdf/kit.cjs, and apps/web/src/generated/theme.css. Colors come from theme tokens.
Security#
Do not open a public issue for a vulnerability. Use private reporting or see SECURITY.md.
Questions#
- Product and "how do I": Discussions
- Handbook for attendees, staff, and clients: wiki
- Hosted-event support: info@eventrunner.org
Required branch and connector checks#
Every change to main must use a pull request. Branch protection applies to administrators,
blocks force pushes and deletion, requires an up-to-date CI gate, and requires resolved
review conversations. Do not bypass or disable these rules to land a repair.
The connector bot reviews pull requests separately from CI. Inspect its completed review, fix verified findings, run the affected checks, push the fixes, and resolve only completed conversations. Check the final diff before merging. Use a focused review when a later fix changes behavior that the completed review did not cover.
GitHub Actions does not poll for connector reviews. The required CI gate and resolved
review conversations remain enforced by branch protection.
When web source changes, regenerate the synthetic content with
node scripts/generate-content.cjs --demo and run its --check mode. Run
node scripts/build-demo.cjs --check to prove the demo builds, builds the same
twice, and fits its budget. Do not commit docs/demo/; it is build output. Regenerate documentation with node scripts/build-pages.cjs after source
Markdown changes. Sign off all repair commits with git commit -s.
Branch rules control Git operations. An administrator credential can still edit those rules. Agents should use a separate identity with write access and no repository or organization administration permission; keep rule administration with the human owner.