Developers
Contributing
Hubzero is an open source project with contributions from many groups and organizations. This book explains how to work on the code in a way that lands cleanly: the conventions the codebase follows, how commits are written, how to run the tests, and how to send a change back.
Reporting problems
Report bugs and security issues at https://help.hubzero.org/support.
Contributions
How a change gets from your working copy into Hubzero. The short version: fork hubzero/hubzero-cms, branch from the release line you are fixing, open a pull request, and say in it what broke and how you tested the fix.
The repository
The CMS is one repository, hubzero/hubzero-cms, with two top-level trees:
| Tree | What it holds |
|---|---|
core/ |
The platform: components, plugins, modules, templates, the Hubzero framework library, migrations, and the muse command-line tool |
app/ |
A hub's own overrides and configuration. Git-ignored; it is a running hub's local state, not shipped code |
A contribution is a change under core/, docs/, tools/ or .github/.
Nothing you write under app/ reaches the repository.
Source files carry an MIT header:
/**
* @package hubzero-cms
* @copyright Copyright © 2026 Purdue University. All Rights Reserved.
* @license http://opensource.org/licenses/MIT MIT
*/
A new file gets the current year. A file you modify keeps the earliest year
already in its header and extends the range:
Copyright © 2015-2026 Purdue University. All Rights Reserved. Do not change
the @license line. The documentation under docs/ is
MIT as well.
Most of core still reads
Copyright (c) 2005-2020 The Regents of the University of California., from an
earlier stewardship of the project. Leave those lines as they are; the change of
holder applies to new work.
The root LICENSE file and core/composer.json both say
MIT, and they agree with the header above. They did not until recently: the
root file was the GNU General Public License version 2 and the manifest
declared GPL-2.0-or-later, while every source file said MIT. The project
settled on MIT and the two outliers were corrected.
Two files outside core/vendor/ remain third-party and stay under the GNU
General Public License, because relicensing someone else's work is not the
project's to do. Both are named at the foot of LICENSE, and their notices
live in the files themselves:
| File | Origin |
|---|---|
core/components/com_wiki/helpers/sanitizer.php |
The MediaWiki XHTML sanitizer, GPL version 2 or later |
core/plugins/user/domainrestriction/helpers/IPv6Net.php |
Copyright (c) 2011 Juergen Enge, GPL version 2 |
Everything under core/vendor/ is installed by Composer and carries whatever
license its package declares.
Third-party code the CMS includes or derives from is listed in
ACKNOWLEDGMENTS.md, which also lists the
contributors.
Branches
Each release line has a X.Y-main branch. The repository carries them back to
1.0-main; the recent ones are 2.2-main, 2.3-main and 2.4-main.
2.4-main is the current line and the repository's default branch. Point
releases are tagged from it.
Branch your work from the release line the fix belongs to, and open the pull request against that same branch. Give the branch a name that means something to you; nothing depends on it.
There are also X.Y-dev branches — 2.4-dev, 2.5-dev, 3.0-dev — carrying
work for lines that are not released. Unless a maintainer asks you to target
one, send a fix to 2.4-main: a change that lands on a -dev branch reaches
nobody's hub until that line ships.
Before you open a pull request
Install the development dependencies once:
(cd core && php bin/composer install)
Then:
(cd core && vendor/bin/parallel-lint --exclude vendor .)
(cd core && vendor/bin/phpunit -c phpunit.xml.dist)
php tools/lint/missing-facade-imports.php
Read PHP Coding Style for what each of those catches and how to run phpcs usefully against a codebase that has no committed ruleset. The suite takes about half a minute; Testing covers running one test while you work on it.
If you touched anything under docs/, rebuild the site and commit the result:
sh tools/docs/rebuild.sh
What the build checks
Three workflows run on a pull request, each on the paths it cares about. A
pull request that touches only docs/ runs the documentation one and nothing
else.
| Workflow | Runs when | Checks |
|---|---|---|
php-lint.yml |
any *.php changes |
php -l on every *.php under core and app outside vendor, then tools/lint/missing-facade-imports.php, then tools/lint/undefined-language-keys.php against a ceiling |
tests.yml |
core/**.php, core/phpunit.xml.dist or the Composer files change |
Installs the Composer dependencies and runs vendor/bin/phpunit -c phpunit.xml.dist |
pages.yml |
docs/, gh-pages/ or tools/docs/ changes |
Runs the documentation builder's tests, regenerates docs/reference and fails if it differs, builds the site, checks every internal link, and fails if the committed gh-pages/public is stale |
A fourth, dev-push.yml, deploys to a
Purdue development host and is switched off.
The facade check is worth understanding before it fails on you. The CMS
registers Route, User, Lang and the rest as root-namespace aliases, so an
unqualified Route::url() inside a namespaced file resolves to
Current\Namespace\Route and fatals when that line runs. The file parses, so
php -l says nothing, and the fault only surfaces on the path that reaches it.
730 of these were fixed at once; the linter keeps them from coming back. Run it
with --fix to insert the missing use statements.
The language-key check is graded, not absolute. Lang::txt() prints the key
when nothing defines it, and 444 keys in the tree are undefined today, so the
workflow runs the linter with --max=444: the build passes at today's number
and fails the moment your change adds one. Define the keys your change asks
for. If you fix some existing ones, lower the ceiling in the workflow in the
same pull request, so the count cannot creep back.
No workflow runs phpcs. Style is caught in review; see When the linter and the house style disagree.
The pull request
The pull request template asks for:
- links to the issue or ticket the change answers
- a summary of the problem in your own words
- a summary of what the change does
- what you specifically did to test it — the part reviewers most often have to ask for
- whether the change needs to reach a production hub before the next core rollout
- a named reviewer
CODEOWNERS is a single line, * @nkissebe, so
every pull request automatically requests a review from the repository's
maintainer, whatever it touches. Whether GitHub then blocks the merge until
that review arrives is branch protection, which is a repository setting and
not a file you can read here. Write for the review either way: the template's
testing question is the one reviewers most often have to send a pull request
back for.
Write the commit messages the way Commit Messages describes: an extension prefix, a sentence, and a body that says why.
What makes a change acceptable
Beyond the conventions:
- It works, and you say how you know. A description of what you tested is worth more than an assertion that you did.
- It is in English. Variables, functions, and comments. Code the maintainers cannot read is code they cannot maintain.
- It is finished. If the feature needs language strings, a migration, or a documentation page, they come with it, not later.
- It is one thing. A behaviour change and a reformatting pass are two commits, and often two pull requests. A reviewer cannot see one through the other.
- It does not widen access by accident. A change to a permission check, an
ACL rule, or a query's
WHEREclause needs to say what it now allows that it did not before. - It is portable. Relative paths, configuration values, and hostnames — no absolute paths from your machine and no IP addresses.
Before proposing a feature rather than a fix, consider:
- Who is it useful to, and can it be switched off by everyone else?
- Is the target audience a hub visitor, a hub manager, or a system administrator, and is the interface right for them?
- What does it cost to maintain?
- Does something similar already exist that could be extended instead?
Reporting problems
- Bugs and feature requests: https://help.hubzero.org/support, or a GitHub issue using the bug report template, which asks for the Hubzero version, the PHP version, the operating system, and steps to reproduce.
- Security issues: email support@hubzero.org. Do not open a public issue.
See
SECURITY.md.
Some faults come from a Composer package rather than from Hubzero. When that happens the maintainers will usually work around it here and point you at the package's own tracker.
A development environment
You need a working hub to develop against; the CMS does not run standalone. Development Environment covers getting one. Installing a hub from scratch is being rewritten for the new web installer; see Installing a hub. The platform targets PHP 8.2.
Once the hub runs, replace its CMS directory with your clone, restore the
app/ directory from the original, install the Composer dependencies, and run
the migrations:
(cd core && php bin/composer install)
php core/bin/muse migration -f
muse migration runs in dry-run mode by default and lists what it would do;
-f is the full run. Older instructions pair -f with -i. That flag is
deprecated: it now behaves as -a, which only widens what is listed.
muse is the CMS command line; it does a great deal more than migrations.
Working on the documentation
The documentation is Markdown under docs/ in the hubzero-cms repository,
built into the site you are reading by the scripts under gh-pages/. It is
edited the same way as the code: on a branch, in a pull request, with the
site rebuilt and checked before it merges.
Editing a page
Every page has an Edit this page on GitHub link in its footer. For anything larger than a typo, clone the repository and work locally:
python3 -m pip install -r gh-pages/requirements.txt -r tools/docs/requirements.txt pytest
sh tools/docs/rebuild.sh
python3 -m http.server -d gh-pages/public 8000
Then open http://localhost:8000/. Rebuild after each change; the build takes a few seconds.
The writing guide covers file naming, the metadata header, links, code blocks, callouts, and house style.
Reviewing an imported page
Most pages were imported from help.hubzero.org and carry a banner until someone checks them against the code. To review one:
- Read the page against the current code on
2.4-main: the controllers, views, andconfig.xmlof the component it describes. Fix or rewrite what has drifted. Replace pasted code with an include directive that pulls the real file. - Regenerate any screenshots from a current hub.
- Change the header to
status: reviewed, addreviewed-againstwith the branch and commit you checked, andreviewedwith the date. - Rebuild, commit
gh-pages/public/with the change, and open a pull request.
The status page shows what is left in each book.
Committing
Commit gh-pages/public/ together with the source change. The Pages
workflow rebuilds the site from source on every push and fails if the
committed copy does not match, so a stale copy cannot be deployed by
accident.
Rewritten and checked against 2.4-main @ 348f0057c2 on 2026-09-10.