Contributing
Submitting pull requests
The Wheels repo accepts contributions through standard GitHub pull requests against the develop branch. The path that gets your change merged fastest is the same one the core team uses: fork, branch, run bash tools/test-local.sh before every push, commit with a type and scope that pass commitlint, open a PR against develop with a filled-out test plan, and respond to review comments as they come in.
You’ll learn:
- How to fork, clone, and wire up an
upstreamremote so you can keep your branch current - How the repo expects branches, commits, and PR bodies to be shaped
- Exactly which commit types and scopes
commitlint.config.jsaccepts — and what happens when you miss - What to expect during review, iteration, and merge
Fork, clone, and set upstream
Section titled “Fork, clone, and set upstream”-
Fork
wheels-dev/wheelson GitHub into your account or org. -
Clone your fork and set the canonical repo as
upstream:your shell git clone git@github.com:YOUR_USERNAME/wheels.gitcd wheelsgit remote add upstream git@github.com:wheels-dev/wheels.gitgit fetch upstream -
Track
developfrom upstream so rebases and pulls are unambiguous:your shell git checkout -B develop upstream/develop
The default target branch for PRs is develop, not main. PRs opened against main get redirected during review.
Create a branch
Section titled “Create a branch”Branch names are a personal namespace — the repo doesn’t enforce a prefix. The core team uses a personal prefix (e.g. peter/*) so their branches are easy to spot in the remote list; contributors commonly use fix/*, feat/*, or their own handle. Pick something short and descriptive.
git checkout -B feat/route-model-binding upstream/developOne logical change per branch. Unrelated fixes belong in separate PRs — they review faster and revert cleanly if something regresses.
Run the tests locally before every push
Section titled “Run the tests locally before every push”This is non-negotiable. CI runs the full cross-engine matrix, but you verify Lucee 7 + SQLite locally before pushing — it catches 90% of issues in under a minute, and it keeps CI queue time healthy for everyone.
bash tools/test-local.sh # full core suitebash tools/test-local.sh model # just model specsbash tools/test-local.sh security # just security specsThe script creates the SQLite test databases, starts a disposable LuCLI server on port 8080 if one isn’t already up, runs the suite, prints a coloured pass/fail summary, and cleans up on exit. See Running Tests Locally for the full command surface, filter aliases, and the Docker cross-engine matrix for pre-merge verification.
Commit with a valid type and scope
Section titled “Commit with a valid type and scope”The repo enforces Conventional Commits via commitlint. The config lives at commitlint.config.js in the repo root. CI rejects PRs whose commits don’t parse.
Shape: type(scope): lowercase subject
Valid types (from @commitlint/config-conventional, referenced in commitlint.config.js line 13):
feat, fix, docs, style, refactor, perf, test, build, ci, chore, revert
Valid scopes (scope-enum allowlist, commitlint.config.js lines 17–44):
model, controller, view, router, middleware, migration, cli, test, config, di, job, mailer, plugin, sse, seed, docs, web, web/ui, web/landing, web/blog, web/guides, web/api, web/starlight
The scope is optional — you may omit the (scope) parenthetical entirely. When present, it must be one of the values above. There is no security scope; use the layer the fix touches (model for a SQL-injection fix, view for XSS, config for consoleeval hardening, cli for MCP server fixes).
Subject casing: only ALL-CAPS is rejected (subject-case rule at line 56). Proper nouns like “Giscus”, “CockroachDB” are fine. Header length caps at 100 characters.
feat(router): add route model bindingfix(model): correct association eager loadingdocs(web/guides): rewrite testing sectiontest: add coverage for rate limiterFeat: add route model binding # type must be lowercasefix(security): patch XSS in forms # security is not an allowed scopeUPDATE MODEL # subject is ALL-CAPSOpen the pull request
Section titled “Open the pull request”Push your branch and open the PR against upstream/develop.
git push -u origin feat/route-model-bindinggh pr create --base develop --webThe repo ships a PR template at .github/PULL_REQUEST_TEMPLATE.md with these required sections:
- Summary — one short paragraph describing what the PR does.
- Related Issue —
Closes #123if the PR addresses a tracked issue; otherwise state that it’s standalone. - Type of Change — tick the matching box (bug fix, new feature, enhancement, documentation, refactoring).
- Feature Completeness Checklist — for new features and enhancements, confirm tests, framework docs,
.ai/wheels/reference docs,CLAUDE.mdupdates if conventions changed, and aCHANGELOG.mdentry under[Unreleased]. - Test Plan — the exact steps a reviewer can run to verify your change. Commands, expected output, URLs to hit. Skipping this is the single biggest cause of review delays.
- Screenshots / Output — for UI-touching or output-changing PRs.
Fill every section. Empty headings telegraph “I didn’t read the template” and slow your review down.
Review, iterate, merge
Section titled “Review, iterate, merge”Expect an initial look from a maintainer within a few business days. Smaller, well-scoped PRs with complete test plans move faster than large, thin ones.
During review you’ll see:
- Inline comments on the diff — respond per-comment; push follow-up commits on the same branch rather than force-pushing. The PR updates automatically.
- Requests for missing pieces — test coverage, a
CHANGELOG.mdentry, a.ai/wheels/doc. These are listed in the template for a reason; add them rather than arguing they don’t apply. - Cross-engine fixes — if CI flags Adobe CF or BoxLang failures a local Lucee run didn’t catch, consult the cross-engine gotcha list in Running Tests Locally. Verify your fix against the local Docker container for that engine before pushing.
Keep your branch up to date if review takes long enough that develop drifts:
git fetch upstreamgit rebase upstream/developgit push --force-with-leaseUse --force-with-lease, not --force. It refuses the push if someone else pushed to your branch meanwhile — protecting co-contributor commits.
When review is satisfied, a maintainer merges. The repo’s current convention is squash-and-merge for feature and fix branches, preserving a single conventional-commit message on develop. You’ll see the merged commit land on develop; your branch on the fork is safe to delete (gh pr checkout offers to delete after merge, or git branch -D locally + delete the remote on GitHub).
After merge
Section titled “After merge”A handful of optional follow-ups keep your local environment clean:
- Delete the merged branch both locally and on your fork.
- Pull
developinto your local clone so your next branch starts from the latest tree. - Check the
[Unreleased]section ofCHANGELOG.md— if your PR added an entry, confirm it rendered correctly. - Watch the release notes — merged PRs surface in the next version’s notes under the matching section (Features, Fixes, Documentation).