Configuration reference¶
cwm-build-tools reads two files. Keep them separate — conflating them is the most common setup mistake.
| File | Committed? | Owns |
|---|---|---|
cwm-build.config.json |
yes | What the project is — layout, manifests, ARS endpoint, profile. No secrets. |
build.properties |
never (gitignored) | Where your local Joomla installs live — paths, URLs, DB/admin creds. Per-developer. |
cwm-build.config.json¶
Committed; consumed by every cwm-* command. Scaffold it with composer
cwm-init rather than authoring by hand. Minimal examples live in
examples/.
Top-level keys¶
| Key | Purpose |
|---|---|
extension |
{ type, name, displayName }. type is the Joomla install type (component / library / plugin / module / package). |
profile |
Archetype that owns the versionTracking shape: component, library, or package-wrapper. Independent of extension.type — pick the one matching how you bump and ship. See profiles. |
manifests |
{ package?, extensions[] }. Each extensions[] entry is { type, path } — the manifest XML for one sub-extension. Drives auto-derived dev links and cwm-verify. |
build |
How cwm-build / cwm-release produce the zip. See build block. |
ars |
Akeeba Release System target: endpoint, categoryId, updateStreamId, environments[], tokenItem, tokenVault (1Password). |
github |
{ owner, repo, releaseBranch } for release + changelog. |
changelog |
{ file, url } — the Joomla changelog XML path and its raw URL. |
lint |
Optional. { paths[], excludeDirs[], skip[], issueDigits } — source roots the lint commands scan, and extra directory basenames to skip on top of vendor/, node_modules/, build/, dist/ and the VCS directories. A git submodule is a separate repository and not this project's standard to enforce; name it in excludeDirs. Defaults to the conventional Joomla roots that exist. skip[] and issueDigits are cwm-lint-comments' equivalents — path fragments to skip (on top of vendor/, node_modules/, .git/, tests/, libraries/, .min.) and how many digits make a token an issue number (default 4; three collide with CSS hex colours, so a repo numbering in the hundreds must lower it). |
testSite |
Optional. { reset: { elements[], elementPatterns[], retain[], tablePrefixes[], components[], menuLinkPatterns[], typeAliasPatterns[], modulePatterns[], updateSiteNamePatterns[], actionLogPatterns[], directories[], fileGlobs[], groups{} } } — what cwm-reset-testsite removes from a role = test install, and what it must not. retain[] overrides a family match and is printed and re-checked after every run, so "this survived on purpose" is distinguishable from "this survived because nobody looked". An elements[] entry may be a plain string (match the element wherever it is) or { element, type?, folder? } to name one of two plugins sharing an element. groups{} holds optional families enabled with --with <name> — for a stack shared with another extension, which must not be removed unasked. |
schemaReplay |
Optional. { prefix?, targets[] } — what cwm-schema-replay executes. Each target is { name, manifest, root?, baseline, from }, where baseline is a path or a list of paths applied in order and from is the version that baseline represents. root is the directory the manifest's relative paths resolve against (Joomla's extension_root), defaulting to the manifest's own directory — set it when a source tree keeps the manifest above the sql/ it names. The baseline is a site's schema, not the extension's install SQL: migrations write to core tables no manifest creates. See the schema replay baseline. |
baseline |
Optional. { minimum } — the oldest release cwm-baseline may pick as an upgrade "before" state. For projects whose early packages do not install at all, so choosing one wastes a run on a failure that says nothing about the build. Omit for no floor. |
announcement |
{ command, bulletsDir } for the release announcement article. |
versionTracking |
Override layer, deep-merged on top of the profile. versionsJson, packageJson, substituteTokens.paths[], sourceFiles[], activeDevelopment. Lists replace wholesale. See source-file version literals and reopening the cycle below. |
versionTracking.sourceFiles — version literals in source¶
For a version hardcoded in code that ships beside the manifest — the shape
public const VERSION = '1.2.3'; — which nothing else in the toolchain writes,
so it drifts. cwm-bump rewrites it along with the manifest:
"versionTracking": {
"sourceFiles": [
{ "path": "src/LibraryVersion.php",
"pattern": "public const VERSION = '{version}';" }
]
}
Why this exists: lib_cwmscripture's LibraryVersion::VERSION sat at 1.1.3 while
its manifest said 1.1.4, and satisfies() / needsUpgrade() read that constant —
so downstream extensions gating on a minimum library version were answered from a
stale number and could refuse a library that was new enough.
Patterns are literals, not regexes. The whole string is quoted and only
{version} becomes a capture, so you paste the line as it appears in the file
and $, ( or . match themselves. That also keeps a pattern reviewable by
someone who does not write regex.
{version} matches a semver-ish token including pre-release and build suffixes
(1.2.3, 1.2.3-beta1, 1.2.3-dev), anchored to digits so a pattern cannot
drift onto arbitrary text. A version-shaped literal on a line the pattern does
not match — an @since tag, an unrelated constant — is left alone.
Unlike the JSON trackers, problems here are fatal rather than warnings:
| Situation | Result |
|---|---|
| Pattern matches nothing | throws, naming the file and pattern |
path missing on disk |
throws |
pattern has no {version} |
throws |
Entry missing path or pattern |
throws |
| File already at the target version | no-op, reported as (no change) |
That is deliberate. A rewrite that silently does nothing is precisely how the
version drifted in the first place, so a misconfigured entry has to stop the
bump rather than let a stale literal ship.
| assets | Source-tree asset staging (images, vendorMediaSource, packages[]). Source paths, not install paths. |
| dev | Optional dev-link overrides — deriveLinks, links[], internalLinks[], cwmSiblings. A links[] entry whose target matches an auto-derived one replaces it, so a single wrong pair can be corrected without setting deriveLinks: false and hand-writing them all. |
| gitignore | { outputPaths[], mediaPaths[] } feeding the managed .gitignore block. |
| vendors | Bundled npm libraries vendor:check reports on — [{ npm, label?, notes? }]. |
| security | Optional vendor:check audit tuning. See security block. |
versionTracking.activeDevelopment — reopening the cycle after a release¶
active_development.version is the pointer devs read when they write an
@since tag by hand. cwm-bump writes the release version into it on the way
out, so the moment a stable release ships it names a version that is already
out — and every @since taken from it afterwards is wrong.
cwm-release step 8 therefore reopens it on the new next.patch, alongside
the current / next.* / _updated writes it already made. Nothing to
configure for the default:
| Key | Default | Purpose |
|---|---|---|
advanceOnRelease |
true |
Move active_development to next.patch after a stable release. |
devSuffix |
"" |
Appended to that value — set "-dev" if your cycles run as 10.5.11-dev. {date} becomes the release date as Ymd and {date:FORMAT} takes any PHP date() format, so "-dev{date}" gives 10.5.11-dev20260819 and "-{date:Y-m-d}-dev" gives 10.5.11-2026-08-19-dev. |
cwm-bump clears the suffix on the way out, writing the plain release version
into the same field, so the cycle runs 10.5.11-dev → 10.5.11 → released →
10.5.12-dev without a manual step. A -dev value never reaches a tag, a zip
or an ARS entry — it only ever lives in this field, and PHP's version_compare
ranks it below even a beta (10.5.11-dev < 10.5.11-beta1 < 10.5.11), which
is where an unshipped cycle belongs.
Before dating it, note that Joomla core deliberately does not: Version::RELDATE
sits beside EXTRA_VERSION = 'rc3-dev' rather than inside it, so anything
comparing versions never has to parse past a date.
Three things are left alone:
- A cycle already open ahead of the release —
10.6.0-devwhile10.5.10ships. Pulling it back to10.5.11is the same wrong@since, pointing the other way. Silent: this is a project doing feature work, not a problem. - A pre-release.
currentandnext.*already stand still for a beta, and the cycle it is stabilising has not shipped yet. - A value that is not a version. Reported on stderr and skipped. Step 8 runs after the GitHub release and the ARS publish, so nothing here throws.
Set advanceOnRelease: false to keep opening cycles by hand. cwm-release
then warns instead, when the pointer is left at or behind the released version:
Warning: active_development.version (10.5.10) is not ahead of the released 10.5.10.
Every @since written from it now names a version that is already out.
Why this is automated rather than a runbook line: the manual step was missed for four consecutive Proclaim releases, and nothing fails when it is. The build succeeds, the release publishes, and the tags are simply wrong in merged code until somebody reads them (#153).
The build block¶
Consumed by cwm-build / cwm-package.
| Key | Purpose |
|---|---|
command |
cwm-build (generic builder) or a project script. |
outputGlob |
Glob cwm-release matches to find the produced zip. |
outputDir, outputName |
Where the zip lands; {version} is substituted. |
manifest |
The extension manifest to read the version from + ship. |
sources[] |
{ from, to } copy pairs (working-tree → zip path). |
excludes[], excludeExtensions[], excludePaths[] |
What to drop; excludeMatchMode is contains or strict. |
includeRoots[], includeRootExtensions[] |
Whitelist filter for from: "." (Proclaim shape). |
vendorPrune |
Strip composer metadata/docs from vendor/ subtrees. |
preBuild |
{ mode: "ensure-minified", dirs[] } or { mode: "run", command } (e.g. npm run build). Runs before zipping. |
verifyAssets |
true to fail the build if a joomla.asset.json-referenced file is missing. See the JS guide. |
verifyMediaSources[] |
{ source, output } directory pairs. Fails the build when a file in output has no matching source in source — i.e. build output that outlived its source. See below. |
versionPrompt |
{ enabled, timeout } for the interactive 3-way version prompt. |
verifyMediaSources — catch build output that outlived its source¶
verifyAssets catches an asset the manifest references that the build never
produced. This catches the opposite: a file in media/ that no source can
reproduce.
"verifyMediaSources": [
{ "source": "build/media_source/js", "output": "media/lib_cwmscripture/js" },
{ "source": "build/media_source/css", "output": "media/lib_cwmscripture/css" }
]
Why it exists: minified output is gitignored, so when a source file is renamed or
deleted, its old output is not removed by any checkout, branch switch or pull. It
just sits in media/, and the packager ships whatever is there. lib_cwmscripture
shipped translations-manager.min.js (plus .gz and .map) in every release for
months after the source became bible-translations.es6.js — the published v1.1.6
zip had 90 files where a fresh build of the same tag had 87. Nothing referenced
them, so there was no error to notice.
The cost of that is not cosmetic: the release artifact stops being a function of the source and becomes a function of the source plus that machine's build history, so two developers on the same tag produce different packages. Fixing it after publication also invalidates the checksums the update server recorded at publish time.
Matching rules:
foo.min.js,foo.min.js.gz,foo.min.js.mapandfoo.jsall trace back tofoo— satisfied byfoo.es6.js,foo.esm.jsorfoo.jsin the source dir.- Files that are not
.js/.mjs/.css(or a.map/.gzof one) are ignored:joomla.asset.json,index.html, licence files. - Only the top level of each
outputdir is checked. Subdirectories are usually copied third-party payloads whose layout has no relationship tomedia_source, and flagging those would just teach people to switch the check off. - A pair whose
outputdir does not exist is skipped; a missingsourcedir is an error, since that is a config typo.
Opt-in per project: a tree that keeps hand-maintained files in the same directory as build output would fail, so point the pairs at the directories your build actually owns.
The security block¶
Consumed by vendor:check (templates/vendor-check.js). Entirely optional —
every key has a working default, so existing configs need no changes.
| Key | Default | Purpose |
|---|---|---|
scanNested |
true |
Discover Composer projects nested inside the repo and audit them too. |
nestedPaths[] |
(unset) | Explicit project dirs relative to the root. Setting this skips auto-discovery entirely — use it when the walk is too slow or picks up something unwanted. |
maxDepth |
6 |
Auto-discovery depth limit. |
ignore[] |
[] |
Advisory IDs to suppress. Matches GHSA, PKSA or CVE. |
Why nested projects matter. An extension may bundle its own Composer
project whose vendor/ tree is committed and ships to end users. Those
dependencies are invisible to a root-level composer outdated, so a vulnerable
bundled package can ship indefinitely without the tool noticing. Proclaim hit
exactly this: vendor:check reported "all up to date" while the bundled YouTube
addon carried a Guzzle with four open advisories.
Auto-discovery skips vendor, node_modules, media, dist and dotfile
directories, and does not descend into a nested project's own subtree.
Audits read the lock file, not the installed tree — an uninstalled project
would otherwise report No packages - skipping audit and pass silently.
Use ignore[] sparingly and only for advisories you have assessed as
inapplicable. Each entry silences a real finding:
Exit codes: 0 clean, 1 updates available, 2 advisories found or
security status unverified (takes precedence over 1). Callers that only test
for non-zero are unaffected.
The check fails closed. If a scope cannot be audited — composer missing
from PATH, a timeout, a broken lock — it is reported as unverified and exits
2 rather than passing. "We found nothing" and "we did not look" are different
answers, and conflating them is how a vulnerable tree earns a green check. A
scope with no composer.json (or, for npm, no package.json + lockfile) is
skipped rather than failed, since there is genuinely nothing to audit.
Trust model
Every value here is author-controlled (committed by the project author). The toolchain treats config + CLI args as trusted; it does not defend against attacker-controlled config values. Secrets never belong here.
build.properties¶
Never committed (gitignored via the managed block). Per-developer; written
by composer cwm-setup. Flat Java-properties keys (IDE-friendly; every key
globally unique).
joomla.version = 5.4.2
builder.installs = j5, j6, j5-test
builder.j5.role = dev # dev | test
builder.j5.path = /path/to/joomla5
builder.j5.url = https://j5-dev.local
builder.j5.version = 5.4.2
builder.j5.admin_user = admin
builder.j5.admin_pass = admin
builder.j5.admin_email = admin@example.com
builder.installs— comma-separated list of install ids; each idXis configured by itsbuilder.X.*keys.role—dev(symlink target) ortest(zip-install target).paths.<package>— flat path keys for cross-package (CWM sibling) resolution.
No database credentials here
cwm-setup used to prompt for db_host / db_user / db_pass /
db_name. It no longer does, and it removes them from a file that has
them. Every command that touches a database resolves credentials from the
install's own configuration.php — that is what the site actually connects
with, and when the two disagree the site is right. A db_pass here was a
password at rest that nothing read.
Files that still carry the keys keep parsing; they are simply not written back.
Format compatibility
The reader accepts the canonical flat format above and the legacy INI
section format ([j5] … ) for backward compatibility. New projects use
flat keys — Java-properties-aware IDEs (PhpStorm/IntelliJ) flag duplicate
keys across INI sections, which flat keys avoid. Use # comments, not ;.