Skip to content

feat(config): introduce BuildInfo.cfc as the runtime version source#2352

Merged
bpamiri merged 3 commits intodevelopfrom
peter/buildinfo-cfc
Apr 28, 2026
Merged

feat(config): introduce BuildInfo.cfc as the runtime version source#2352
bpamiri merged 3 commits intodevelopfrom
peter/buildinfo-cfc

Conversation

@bpamiri
Copy link
Copy Markdown
Collaborator

@bpamiri bpamiri commented Apr 28, 2026

Summary

Replaces the historical pattern of reading the framework's version from vendor/wheels/box.json with a dedicated BuildInfo component. box.json remains for the engine-db matrix tooling but is no longer the runtime source of truth. This decouples version reporting from the ForgeBox manifest format that's being phased out, and surfaces richer build metadata for diagnostics.

What's in BuildInfo

10 fields, all populated by the release pipeline via sed substitution at artifact-construction time:

Field Example value
version 4.0.0-SNAPSHOT+1628
buildNumber 1628
branch develop
commitSha 81e9f7958d3abc...
commitShortSha 81e9f79
commitSubject fix(cli): override version() and showHelp()
builtAt 2026-04-28T19:11:00Z
runId 25072250128
runUrl https://github.com/wheels-dev/wheels/actions/runs/25072250128
repository wheels-dev/wheels

Dev checkouts ship the unresolved placeholders, which version() reports as 0.0.0-dev and the metadata getters blank out.

Wiring

  • Global.cfc:$readFrameworkVersion is now a one-line delegate to new wheels.BuildInfo().version(). The path-arg-based box.json fallback chain is gone.
  • onapplicationstart.cfc instantiates BuildInfo once and caches on application.$wheels.buildInfo. Single instance shared across requests; values can't change without a full server restart.
  • PackageLoader.$normalizeWheelsVersion and Plugins.$normalizeWheelsVersion now treat both @build.version@ and 0.0.0-dev as semver 0.0.0 so strict version constraints don't falsely reject packages on dev builds.
  • tools/build/scripts/prepare-core.sh substitutes the new placeholders during wheels-core artifact construction. Commit info comes from git rev-parse / git log in the source checkout; run context comes from GITHUB_RUN_ID and GITHUB_REPOSITORY env vars (empty locally → BuildInfo blanks the field).

Tests

  • New buildInfoSpec.cfc with focused unit tests for version() sentinel, isDev()/isSnapshot(), all metadata getters, and asStruct().
  • frameworkVersionSpec.cfc trimmed from 14 box.json-fallback-chain tests to 4 boot-time integration assertions. The legacy fallback paths (monorepo detection, wheels-base-template recovery) are no longer needed — BuildInfo's substituted values are authoritative.

What's deliberately NOT in this PR

  • box.json keeps its version field. It's still substituted by the release pipeline (and may still be referenced by the engine-db matrix tooling). We just don't read it anymore for runtime version reporting.
  • FrameworkInstaller.rewriteVersionPlaceholder unchanged. It still rewrites @build.version@ in vendor/wheels/box.json at scaffold time. BuildInfo.cfc on the brew/choco-installed framework is already substituted by the release pipeline.
  • wheels info and the dev toolbar still show only the version string. Surfacing the richer BuildInfo fields in the toolbar is a follow-up — easy now that the data is available.

Test plan

  • bash tools/test-local.sh — 3347 passed, 0 failed
  • bash tools/test-cli-local.sh — 462 passed, 3 failed (pre-existing DoctorSpec packages: doctor mixin-collision scan is regex-based and may miss metadata-driven mixins #2260 issues, unrelated)
  • Local prepare-core.sh dry run produces a fully-substituted BuildInfo.cfc with real git SHA, commit subject, ISO timestamp, run URL, and repository.
  • After merge + a release run, verify on a fresh brew install that the dev toolbar shows the substituted version (no more 0.0.0-dev).

Closes the framework-side half of the "0.0.0-dev debug bar on installed apps" finding from the fresh-VM journal.

bpamiri added 2 commits April 28, 2026 12:01
…tput

Replaces BaseModule's auto-discovered help and bare LuCLI version string
with hand-written banner + grouped command listing. `wheels version` and
`wheels help` now produce the same output as `wheels --version` and
`wheels --help` from the brew/choco wrapper.

Internal callers of version() (info(), console(), copyFrameworkToVendor)
switched to super.version() for the bare string.

Adds cli/lucli/ARCHITECTURE.md documenting the three-layer command flow
(wrapper -> LuCLI -> Module.cfc) so future maintainers know where to
intercept what. The wrapper layer is the only point upstream of picocli,
which absorbs --version/--help during arg parsing before module dispatch.

Pairs with homebrew-wheels and chocolatey-wheels wrapper changes that
intercept --version/--help (see ARCHITECTURE.md for the full picture).
Replaces the historical pattern of reading the framework's version from
vendor/wheels/box.json with a dedicated BuildInfo component. box.json
remains for the engine-db matrix tooling but is no longer the runtime
source of truth. This decouples version reporting from the ForgeBox
manifest format that's being phased out.

The CFC carries 10 build fields, all populated by the release pipeline
via sed substitution:

  version, buildNumber, branch, commitSha, commitShortSha, commitSubject,
  builtAt, runId, runUrl, repository

Released artifacts surface concrete values; dev checkouts ship the
unresolved @build.*@ placeholders, which version() reports as the
"0.0.0-dev" sentinel and the metadata getters blank out.

Cached on application.$wheels.buildInfo at app start. Values cannot
change without a full server restart, so a single instance is shared
across all requests.

Wiring:

- Global.cfc:$readFrameworkVersion is now a one-line delegate to
  BuildInfo.version(), preserving the existing call signature for
  legacy callers (PackageLoader, Plugins, tests).
- onapplicationstart.cfc instantiates BuildInfo once and caches it.
- PackageLoader.$normalizeWheelsVersion and Plugins.$normalizeWheelsVersion
  now treat both "@build.version@" and "0.0.0-dev" as semver 0.0.0 so
  strict version constraints don't falsely reject packages on dev builds.
- prepare-core.sh substitutes the new placeholders during wheels-core
  artifact construction, drawing commit info from git and run context
  from GITHUB_RUN_ID / GITHUB_REPOSITORY env vars.

Tests:

- New buildInfoSpec.cfc with focused unit tests for version sentinel,
  isDev/isSnapshot, metadata getters, and asStruct.
- frameworkVersionSpec.cfc trimmed from 14 box.json-fallback-chain tests
  to 4 boot-time integration assertions. The legacy fallback paths
  (monorepo detection, wheels-base-template recovery) are no longer
  needed -- BuildInfo's substituted values are authoritative.

Verified:
- bash tools/test-local.sh: 3347 passed, 0 failed
- bash tools/test-cli-local.sh: 462 passed, 3 failed (pre-existing
  DoctorSpec #2260 issues, unrelated)
- Local prepare-core.sh dry run produces a fully-substituted BuildInfo.cfc
  with real git SHA, commit subject, ISO timestamp, and run URL.

Closes the framework-side half of the "0.0.0-dev debug bar on installed
apps" finding from the fresh-VM journal.
… test

onapplicationstart.cfc:349 deletes application.$wheels after copying it
to application.wheels. The new test was reading the pre-delete scope,
which works locally only because the dev server's reload behavior leaves
$wheels in place under some conditions but doesn't on a fresh CI boot.
@bpamiri bpamiri merged commit 6cd88d8 into develop Apr 28, 2026
4 checks passed
@bpamiri bpamiri deleted the peter/buildinfo-cfc branch April 28, 2026 21:17
bpamiri added a commit that referenced this pull request Apr 29, 2026
* fix(cli): cfml-escape commit subject baked into BuildInfo.cfc

prepare-core.sh sed-substitutes the latest commit subject into
BuildInfo.cfc inside a double-quoted CFML string. # is CFML's
variable-interpolation delimiter, so a commit subject ending in a
parenthesized PR ref like "(#2352)" produces a syntactically broken
.cfc that crashes Lucee on the first request — bricking every fresh
brew/chocolatey install. Onboarding finding F1.

Escape both # → ## and " → "" before the substitution, then escape sed
replacement specials (\, &) so the value transports through the
existing sed pipeline cleanly. Add an inline post-substitution check
that fails the build if any unescaped # or " remains in the produced
commitSubject literal — a regression-stop without standing up a new
test harness.

Verified end-to-end: with COMMIT_SUBJECT="feat(config): introduce
BuildInfo.cfc as the runtime version source (#2352)", the produced
BuildInfo.cfc reads `commitSubject: "...source (##2352)",` (valid
CFML); reverting the escape pipeline triggers the self-check with
exit 1.

* fix(cli): scaffold writes plural resource name to routes.cfm

generateScaffold() takes a singular model name (e.g. "Post") and
correctly derives a pluralized form for the controller, views, and
migration — but updateRoutes() was being passed the raw singular,
producing .resources("post") in routes.cfm. Onboarding finding F4.

Wheels' resource convention is plural (PostsController maps to
.resources("posts")), so the singular variant either creates a phantom
second route that conflicts with any hand-added plural route, or
silently fails to bind to the controller scaffolded a few lines
earlier. The scaffold runs through cleanly otherwise — this is the one
piece that drifted from the convention.

Pass pluralName, which is already computed for the model/controller/
view generation right above. Add a regression spec that asserts
routes.cfm contains .resources("articles") (plural) and not
.resources("article") (singular) after scaffolding "Article".

* fix(cli): also stage sqlite driver into per-server lucee-server/bundles/

The previous fix (#2349) staged the SQLite JDBC driver into every
Lucee Express install's lib/ext/ — Tomcat's parent classpath. That
satisfies Class.forName("org.sqlite.JDBC") but Lucee 7's datasource
resolver consults its OSGi bundle loader for driver instantiation,
not the Tomcat classloader. Without the bundle in
<server>/lucee-server/bundles/, the very first cfquery against a
SQLite datasource fails — and the Wheels migrator goes through
cfquery, so `wheels migrate latest` blows up on every fresh app.
Onboarding finding F2.

Other engines (MySQL, Postgres) avoid the issue because their bundles
ship inside lucee-7.x.x.jar and Lucee auto-deploys them; the SQLite
extension is third-party and doesn't get the same treatment, so
seeding bundles/ is on us.

Add BundleStager.stageIntoServerBundles() alongside the existing
stageIntoLibExt() — same idempotent / best-effort contract, walks
LUCLI_HOME/servers/<name>/lucee-server/bundles/. Wire it into
$ensureWheelsBundles() so every wheels start populates both targets.
The OSGi-symbolic-name filename (org.xerial.sqlite-jdbc-3.49.1.0.jar)
is required for Lucee's bundle loader to recognize it.

5 new specs cover detection, idempotency, missing serversRoot,
missing bundleSrc, and partial-server-context tolerance. CLI suite
462 → 467 pass on this worktree.

* fix(cli): wheels reload purges per-server cfclasses cache

LuCLI's dev-server Lucee config defaults to `inspectTemplate=once`,
which means Lucee compiles each CFC once, caches the .class on disk
under `<server>/lucee-server/context/cfclasses/`, and never rechecks
the source timestamp. Wheels' `?reload=true` resets framework state
via applicationStop() but does not invalidate Lucee's template cache,
so source edits to models, controllers, and config silently keep
returning the old compiled class. Even `wheels stop && wheels start`
doesn't help — the .class on disk persists. The only path that picks
up changes is physically wiping cfclasses. Onboarding finding F5; the
single-biggest time sink across the tutorial run.

`wheels reload` now physically wipes the cfclasses dir for the
project's registered server before triggering the HTTP reload. Order
matters: purge first, then reload — the application reset on the
next request gets the fresh compile.

Extract the deletion into CfclassesPurger.cfc (matching the
BundleStager extraction pattern) so the logic is unit-testable
without touching a live server. 5 new specs cover happy path, dir
preservation, missing path, empty path, and idempotency.

Best-effort: per-file failures (Lucee-locked .class mid-compile,
Windows file lock) are swallowed rather than aborting the reload —
the user is better served by a partial-purge than a full crash.

* fix(cli): refuse wheels start/stop in non-wheels-project directories

LuCLI's `server start` and `server stop` derive the server name from
the cwd basename when no registered server matches. Running `wheels
start` or `wheels stop` from a parent directory or unrelated dir
(easy to do by accident, especially in shell sessions where cwd
resets between commands) silently registers a phantom server context
named after that dir. The user ends up with ghost entries like `ws`
or `Downloads` in `wheels server list`, with `<unknown>` webroots
and no path to clean up. Onboarding finding F6.

Add a `$isWheelsProjectDir(path)` helper that checks for
`config/settings.cfm` — the file `wheels new` always writes and that
no other tool creates (box.json and Application.cfc are not specific
enough).

  - `wheels start` refuses up-front when not in a Wheels project,
    pointing the user at `wheels new`.
  - `wheels stop` already had a guard for the case where the cwd has
    no registered server but other servers are running (reports them);
    extend it to also refuse fallthrough when no servers are running
    at all, so we never reach LuCLI's name-from-cwd code path.

* fix(cli): wheels browser test defaults to app dir and app test runner

Two bugs in `wheels browser test` made it useless from the app for
running browser specs. Onboarding finding F11.

  - Default directory was `wheels.tests.specs.wheelstest`, the
    framework's internal browser-DSL test directory. Apps live at
    `tests/specs/browser/`, so the runner reported 0 tests against
    a fresh app spec.

  - Test URL hit `/wheels/core/tests`, the framework's core test
    runner. That endpoint only knows about specs under
    `vendor/wheels/tests/specs/`. App-side specs are mounted by
    `/wheels/app/tests`, the app test runner.

Default directory is now `tests.specs.browser` and the runner hits
`/wheels/app/tests`. `--directory=...` still overrides for advanced
use.

Two related sub-issues from the same finding (running browser specs
corrupts the dev server's cfclasses cache for other specs;
WHEELS_ENV=test wheels migrate latest looks for an unconfigured
`wheelstestdb_sqlite` datasource) are out of scope for this fix and
tracked separately.
bpamiri added a commit that referenced this pull request Apr 29, 2026
…substitution (#2368)

The release pipeline (tools/build/scripts/prepare-core.sh) does a global
sed pass `s/@build.version@/<version>/g` over BuildInfo.cfc at artifact-
construction time. Before this fix, isDev() compared variables.info.version
to the literal "@build.version@" string — sed rewrote that literal too,
silently turning every released build into a self-reported "0.0.0-dev"
build (debug bar, /info, asStruct(), and downstream consumers all wrong).

Replace the literal-equality check with the same prefix/suffix structural
check that $blankIfPlaceholder() already uses ("@build." + "@"). This is
invariant under the substitution because the matcher never embeds the full
sentinel string. Mirrors what BuildInfo.cfc was already doing for every
other placeholder in the file.

Add a regression-guard test that reads the source and asserts the literal
"@build.version@" appears exactly once (only in the variables.info.version
field). A second occurrence anywhere — comments, comparison logic, anything
sed sees — gets rewritten and re-introduces the bug. The test would have
caught this when #2352 first introduced BuildInfo.cfc.

The bug has been silently present in every snapshot since #2352 merged
(2026-04-28) — the same PR that introduced both BuildInfo.cfc and the
substitution block in prepare-core.sh. Unit tests in buildInfoSpec.cfc run
against the source (placeholder still literal), where isDev() returns the
right answer; only post-substitution artifacts trigger the bug.

Verified end-to-end on the fresh-VM blog app:

    pre-fix:  application.wheels.buildInfo.version() = "0.0.0-dev"
              debug bar:                              "Wheels 0.0.0-dev"
    post-fix: application.wheels.buildInfo.version() = "4.0.0-SNAPSHOT+1644"
              debug bar:                              "Wheels 4.0.0-SNAPSHOT+1644"

(Verified by sed-substituting the fixed source locally, scp'ing the result
to the VM's blog/vendor/wheels/, and inspecting the rendered debug bar.)

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant