feat(config): introduce BuildInfo.cfc as the runtime version source#2352
Merged
feat(config): introduce BuildInfo.cfc as the runtime version source#2352
Conversation
…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
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.
4 tasks
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>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Replaces the historical pattern of reading the framework's version from
vendor/wheels/box.jsonwith a dedicatedBuildInfocomponent.box.jsonremains 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:
version4.0.0-SNAPSHOT+1628buildNumber1628branchdevelopcommitSha81e9f7958d3abc...commitShortSha81e9f79commitSubjectfix(cli): override version() and showHelp()builtAt2026-04-28T19:11:00ZrunId25072250128runUrlhttps://github.com/wheels-dev/wheels/actions/runs/25072250128repositorywheels-dev/wheelsDev checkouts ship the unresolved placeholders, which
version()reports as0.0.0-devand the metadata getters blank out.Wiring
Global.cfc:$readFrameworkVersionis now a one-line delegate tonew wheels.BuildInfo().version(). The path-arg-based box.json fallback chain is gone.onapplicationstart.cfcinstantiates BuildInfo once and caches onapplication.$wheels.buildInfo. Single instance shared across requests; values can't change without a full server restart.PackageLoader.$normalizeWheelsVersionandPlugins.$normalizeWheelsVersionnow treat both@build.version@and0.0.0-devas semver0.0.0so strict version constraints don't falsely reject packages on dev builds.tools/build/scripts/prepare-core.shsubstitutes the new placeholders during wheels-core artifact construction. Commit info comes fromgit rev-parse/git login the source checkout; run context comes fromGITHUB_RUN_IDandGITHUB_REPOSITORYenv vars (empty locally → BuildInfo blanks the field).Tests
buildInfoSpec.cfcwith focused unit tests forversion()sentinel,isDev()/isSnapshot(), all metadata getters, andasStruct().frameworkVersionSpec.cfctrimmed 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.jsonkeeps itsversionfield. 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.rewriteVersionPlaceholderunchanged. It still rewrites@build.version@invendor/wheels/box.jsonat scaffold time. BuildInfo.cfc on the brew/choco-installed framework is already substituted by the release pipeline.wheels infoand 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 failedbash tools/test-cli-local.sh— 462 passed, 3 failed (pre-existingDoctorSpecpackages: doctor mixin-collision scan is regex-based and may miss metadata-driven mixins #2260 issues, unrelated)prepare-core.shdry run produces a fully-substitutedBuildInfo.cfcwith real git SHA, commit subject, ISO timestamp, run URL, and repository.brew installthat the dev toolbar shows the substituted version (no more0.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.