This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
Apache Allura is an open source software "forge" (à la SourceForge/GitHub): source repo hosting (Git/SVN/Hg),
ticket tracker, wiki, discussion forums, blog, and more, organized into projects/neighborhoods. It's a Python
web app built on TurboGears 2, MongoDB (via the Ming ODM), and Solr for search — extended via a plugin
("tool") architecture.
This is a multi-package monorepo. Each top-level directory is an independently pip install-able package:
Allura/ — the core platform: web framework glue, models, auth, permissions, task/event bus, baseAlluraTest/ — shared test harness/fixtures (alluratest.controller) used by every other package's tests.Forge*/ (ForgeTracker, ForgeWiki, ForgeDiscussion, ForgeGit, ForgeSVN, ForgeBlog, ForgeChat, ForgeActivity,setup.py/pyproject.toml, own package dirforgetracker/), own tests, and its own *.egg-info.scm_config/, solr_config/ — config for git/http and Solr, used by the Docker dev environment.scripts/ — one-off admin/migration scripts (run with the app's Python env), e.g.scripts/convert_encrypted_field.py, scripts/migrations/.Sibling repos extend this one as plugins rather than living in this monorepo: SourceForge's commercial
layer forge-classic (dev checkout typically /src/forge-classic, deployed /var/local/forge-classic)
and its theme sftheme (/src/sftheme, /var/local/sftheme). This repo has zero references back to
either — the dependency runs one-way, entirely through the entry-point mechanism described below. See
"Working with sibling extension repos" further down for how to debug across that boundary.
Each Forge package registers itself with the core platform via entry points declared in its
pyproject.toml (older packages may still show these in setup.cfg/*.egg-info), most importantly:
[project.entry-points.allura]
Tickets = "forgetracker.tracker_main:ForgeTrackerApp"
To find the root controller for a tool, look up its [project.entry-points.allura] entry, find that
Application subclass, and look at its root attribute.
Full install instructions are in Allura/docs/getting_started/installation.rst (or the Docker equivalent
install_each_step.rst/README) — not worth duplicating here. In short, a working system needs MongoDB, Solr,
and the Allura web app + taskd background worker all running. docker-compose.yml defines all of these
services (web, taskd, solr, mongo, outmail, inmail, http).
Key .ini config files live in Allura/: development.ini (local dev), test.ini (tests),
docker-dev.ini (docker dev), production-docker-example.ini.
To run the web/taskd processes in the foreground for interactive debugging (e.g. with ipdb):
cd Allura
gunicorn --reload --paste development.ini -b :8080 # web
paster taskd development.ini --nocapture # taskd (background tasks/events)
Docker equivalents are in Allura/docs/development/contributing.rst.
Tests use pytest. Run everything from the repo root:
./run_tests # all packages, in parallel, plus npm run lint-es6
./run_tests -p ForgeTracker # just one package
./run_tests --coverage # with coverage
./run_tests -n X # number of processes to be used per suite
./run_tests -m X # number of parallel processes to be used per suite
Or run a single package/test directly (faster while iterating):
cd ForgeTracker && pytest
cd ForgeTracker && pytest forgetracker/tests/functional/test_root.py::TestRootController::test_new_ticket -v
Notes:
AlluraTest always runs first (it's imported by everything else; catches syntax errors early).ForgeGit and ForgeSVN are NOT safe to run with pytest-xdist multiprocessing (run_tests handlesallura.tests.TestController; its self.app is a WebTest app withc.project preset to the test project and c.user preset to test-admin. Tools under test are/<entry point name>/.c/g but not a full WSGI app usealluratest.controller.setup_unit_test() + allura.lib.helpers.set_context(...).@task/@event_handler) — test themtaskd.Allura/docs/development/testing.rst for more detail.ruff (config in ruff.toml, line length 119, py310 target). Notable repo-wide conventions.utcnow()/.utcfromtimestamp()/calendar.timegm(), never.now()/.fromtimestamp()/.mktime() (timezone-safety rule, checked by pre-commit's local tz-functionstest_syntax.py).npm run lint-es5, npm run lint-es6 (or npm run lint for both).j2lint via pre-commit..pre-commit-config.yaml is the source of truth for all of the above; run pre-commit run --all-filesJS/CSS is built with Broccoli + Babel (ES6) and node-sass, orchestrated via Brocfile.js/package.json:
npm run watch # transpile ES6 JS on change (needed while editing frontend code)
npm run css-watch # compile SCSS on change
npm run build # one-off JS build
Uses React + ES6 for newer frontend code; jQuery/plain JS + Jinja2 templates for older/most pages.
Request context (c / g) — TurboGears sets up a per-request context object c with c.project,
c.app (current tool instance), and c.user. g is the app-globals singleton (allura.lib.app_globals).
allura.lib.helpers.set_context(project_id, mount_point=..., app_config_id=...) and the push_config
context manager are how you change/restore these outside of normal request dispatch (e.g. in scripts/tests).
Request dispatch for everything starts at Allura/allura/controllers/root.py (RootController), which
does neighborhood/project lookup via _lookup, then dispatches into the matched tool's own root controller.
Tools (allura.app.Application) — Each Forge package's main class subclasses Application and is the
extension point tying a plugin into a project: it defines the tool's root controller, permissions, sitemap
entries, config options, and lifecycle hooks. Registered via the [project.entry-points.allura] entry point.
Artifacts (allura.model.artifact.Artifact) — The base class for anything a tool stores that should be
searchable/linkable (wiki pages, tickets, forum posts, commits, etc). Subclassing it gets you automatic Solr
indexing, shortlink generation ([#151], [MyWikiPage], cross-tool via [mount_point:#151]), and ACL
support. Override url(), index(), shorthand_id() at minimum.
Ming ODM (MongoDB) — All persistence is through Ming (allura/model/), which mimics native MongoDB query
syntax closely. session.py and __mongometa__ per-model define collection/index config.
Async processing: tasks vs. events (Allura/docs/platform/message_bus.rst) — Two decorators:
@task — a plain function; .post(*args, **kwargs) schedules it as a MonQTask doc in Mongo for thetaskd daemon to execute later; calling it directly (no .post) just runs it synchronously (used a lot@event_handler('name') — fan-out: many handlers can listen to the same named event.g.post_event('name', *args, **kwargs) schedules the fan-out task; the event name is always the handler'staskd (paster command) is the worker process that executes both.Permissions (Allura/docs/platform/permissions.rst) — Users belong to per-project groups/roles, each
with a list of permission strings. Permissions are additive down the project/subproject hierarchy and via
per-artifact ACLs — child scopes can only grant more access, never revoke inherited access. Checked via
predicate functions like has_project_access(obj, permission) / allura.lib.security.has_access.
Email integration — Every tool mounted in a project gets its own inbound email address
(<topic>@<mount_point>[.<subproject>].<project>.<domain>); inbound mail is parsed and routed to
c.app.handle_message(topic, msg) after an c.app.has_access(user, topic) check. Outbound mail is sent by
posting the allura.tasks.mail_tasks.sendmail task (do not use smtplib directly).
Markdown macros — Custom [[macro_name]] commands in Markdown content. Defined with
@macro() from allura.lib.macro and registered via a [project.entry-points."allura.macros"] entry point;
see allura.lib.macro for existing examples.
Extension points — Besides tools/artifacts, Allura supports pluggable theme providers, auth providers,
project-registration providers, spam filters, phone/2FA services, WSGI middleware, and more — all wired via
entry points. Full list in Allura/docs/development/extending.rst.
allura.app.Application, then register it under[project.entry-points.allura] in that package's pyproject.toml (e.g. Tickets =
"forgetracker.tracker_main:ForgeTrackerApp"). Code alone doesn't register a tool — without the entryg.entry_points['tool'] never sees it (see Allura/allura/lib/app_globals.py:346-352) and it@macro() from allura.lib.macro, register it[project.entry-points."allura.macros"]. The entry point name is arbitrary; the macro is[[hello]]). SeeAllura/docs/development/extending.rst for the full list of extension points (auth providers, themeallura.timers, etc.) — mostallura/allura.* group (e.g. a sibling repo overriding a stock tool), iter_entry_pointsAllura/allura/lib/helpers.py:1193-1237) requires one registered class to be a subclass of theImportError('Ambiguous ... entry points ...') at the point somethingdisable_entry_points.<group> = name1,
name2 in the .ini (see examples in Allura/development.ini:519-525, e.g.disable_entry_points.allura.importers = trac-tickets). Useful for turning off a stock orallura.lib.helpers.set_context(project_id, mount_point=...,
app_config_id=..., neighborhood=...) sets c.project/c.app outside normal request dispatch; thepush_config(obj, **kw) context manager temporarily overrides attributes on any object (e.g. c, g,scripts/migrations/): numbered NNN-description.{py,js,sh} files, run once.js files are raw mongo/mongosh scripts; .py/.shpaster script <ini> <script.py> -- <args> (see "Debugging" below).-field-encryption.sh-style script that converts...-cleanup.sh companion that removes the old unencrypted copies once thescripts/migrations/037-field-encryption.sh /038-field-encryption-cleanup.sh, both thin wrappers aroundscripts/convert_encrypted_field.py). Follow that shape (convert now, clean up in a later,Allura/allura/controllers/root.py. If the request is handled by a tool, find the tool's entry point[project.entry-points.allura] in its pyproject.toml/setup.py, load the Applicationroot attribute — that's the tool's root controller. FailingAllura/docs/development/contributing.rst has more detail.)ipdb): insert import ipdb; ipdb.set_trace() at the line of interest,bash
cd Allura
pkill -f gunicorn; gunicorn --reload --paste development.ini -b :8080 # web
pkill -f taskd; paster taskd development.ini --nocapture # taskdbash
docker compose run --rm web pip install ipdb
docker compose stop web taskd
docker compose run --rm --service-ports web gunicorn --reload --paste Allura/docker-dev.ini -b :8088
docker compose run --rm taskd paster taskd docker-dev.ini --nocapturepaster script <ini> <script.py> -- <args> (implemented byallura.command.script:ScriptCommand, registered under [project.entry-points."paste.paster_command"]Allura/pyproject.toml) loads the full app config (DB bindings, TG globals, logging) and executes--pdb to drop into pdb.post_mortem() on anGlobals.__init__ caches every entry-point group once, perg.entry_points (Allura/allura/lib/app_globals.py:328-338, _cache_eps) — and thatep.load(), only logging Could not load entry
point [%s] %s (resources.py similarly logs Cannot import entry point %s for static-resourceg.entry_points[...], check the web/taskd process logs for those messages beforeg is built once at process startupAllura/allura/lib/app_globals.py:212-217): adding/editing an entry point (even via pip install -e)--reload) is not enough. Escalating checks, frombash
# 1. is it discoverable at all, independent of Allura's own filtering?
python -c "import importlib.metadata as m; [print(ep) for ep in m.entry_points(group='allura.theme.override')]"python
# 2. is Allura's own loader (dedup/subclass/disable filtering) seeing it? run inside
# `paster script development.ini <tmp_script.py>` (see docs/getting_started/administration.rst)
from allura.lib import helpers as h
list(h.iter_entry_points('allura.theme.override'))
list(h.iter_entry_points('allura.admin'))
# or inspect the live cache directly:
import tg; tg.app_globals.entry_points['admin']paster shell/REPL command registered in this stack for ad hoc poking — use a throwawayAllura/scripts/ with paster script instead.forge-classic layers SourceForge-specific tools, overrides, and business logic on top of this Allura
checkout purely via the entry-point mechanisms above — see its own CLAUDE.md for what it registers.
From this side, a few things make debugging across that boundary easier:
override/ directory (e.g. forge-classic'sForgeSF/forgesf/override/) is spliced into the Jinja search path byallura.lib.package_path_loader.PackagePathLoader, driven by that package's[project.entry-points."allura.theme.override"] registration. Default resolution order puts eachallura path (_load_paths,Allura/allura/lib/package_path_loader.py:162-174); a package can reorder itself relative to anotherallura via a template_path_rules = [['>', 'other-ep-name']] class attribute>/</=, see the module docstring at the top of that file for the full rule syntax). If a pagedisable_template_overrides = true in the .ini temporarilytemplate_path_rules/override ordering rather thanpaster script:python
from allura.lib.package_path_loader import PackagePathLoader
PackagePathLoader().init_paths() # ordered list of override dirs actually being searchedoverride/<top-level-package-of-the-module>/templates/... (see the "magic directory" example inpackage_path_loader.py's module docstring), not override/<full.dotted.module>/....override/ files are plain Pythonrepo_monkey_patching.py, imported eagerly from its package __init__.py, is one example). Ifallura.lib.helpers.monkeypatch(*objs) (used internally inallura/lib/patches.py), which only takes effect once its module is actually imported — if a__init__.py, or of an allura.command_init/allura.macros entry-point load) beforerequirements.txt is compiled independently, so their installed dependency versions can driftAllura/test.ini via ConfigParser's use = config:... inheritance — when a test only fails in the.ini files to see what's inherited vs. locally overridden before assumingiter_entry_points subclassImportError at runtime.test_syntax.py, not by ruff itself.rat-excludes.txt lists what'sAllura/allura/model/auth.py and scripts/convert_encrypted_field.py for the pattern used when adding new