Skip to content

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 upstream remote 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.js accepts — and what happens when you miss
  • What to expect during review, iteration, and merge
  1. Fork wheels-dev/wheels on GitHub into your account or org.

  2. Clone your fork and set the canonical repo as upstream:

    your shell
    git clone git@github.com:YOUR_USERNAME/wheels.git
    cd wheels
    git remote add upstream git@github.com:wheels-dev/wheels.git
    git fetch upstream
  3. Track develop from 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.

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.

your shell
git checkout -B feat/route-model-binding upstream/develop

One logical change per branch. Unrelated fixes belong in separate PRs — they review faster and revert cleanly if something regresses.

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.

your shell — in the repo root
bash tools/test-local.sh # full core suite
bash tools/test-local.sh model # just model specs
bash tools/test-local.sh security # just security specs

The 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.

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.

examples that pass
feat(router): add route model binding
fix(model): correct association eager loading
docs(web/guides): rewrite testing section
test: add coverage for rate limiter
examples that get rejected
Feat: add route model binding # type must be lowercase
fix(security): patch XSS in forms # security is not an allowed scope
UPDATE MODEL # subject is ALL-CAPS

Push your branch and open the PR against upstream/develop.

your shell
git push -u origin feat/route-model-binding
gh pr create --base develop --web

The 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 IssueCloses #123 if 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.md updates if conventions changed, and a CHANGELOG.md entry 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.

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.md entry, 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:

your shell
git fetch upstream
git rebase upstream/develop
git push --force-with-lease

Use --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).

A handful of optional follow-ups keep your local environment clean:

  • Delete the merged branch both locally and on your fork.
  • Pull develop into your local clone so your next branch starts from the latest tree.
  • Check the [Unreleased] section of CHANGELOG.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).