Conversation
…substitution
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>
3 tasks
bpamiri
added a commit
that referenced
this pull request
Apr 29, 2026
Adds tools/auto-merge.sh, a self-contained bash script that evaluates a
PR against the project author's standing merge authorization rules and
merges when every gate passes. Replaces the prior "agent reads
CLAUDE.local.md prose, interprets gates by hand, decides whether to
merge" workflow with a deterministic shell predicate that can be wired
into a scheduled task / cron loop without further human review.
Designed for two surfaces:
1. Interactive: `tools/auto-merge.sh <pr>` evaluates one PR and
merges if eligible. `--check` makes it print-only.
2. Unattended: `tools/auto-merge.sh --all --check` lists the
current user's open PRs, prints per-gate diagnostics, exits 1
when nothing is eligible. Drop the `--check` to make it merge
for real. Suitable for a 10-minute cron tick.
Eight gates, evaluated in fail-fast order:
1. PR is OPEN
2. PR author == current `gh api user .login` (refuse to merge
other people's PRs even if they share the org)
3. baseRefName == develop (NEVER main / master / feature branches)
4. mergeable == MERGEABLE (no merge conflicts)
5. mergeStateStatus == CLEAN (branch up to date with base, full
check suite green, no required reviews outstanding)
6. reviewRequests is empty (no reviewer pending a verdict)
7. reviewDecision != CHANGES_REQUESTED
8. Every check in statusCheckRollup is conclusion=SUCCESS — no
FAILURE / IN_PROGRESS / PENDING / QUEUED / CANCELLED
When all eight pass, the script classifies the PR into trivia tier
(every changed file is top-level *.md, .gitignore, .gitattributes, or
.editorconfig) or code tier (anything else, including docs/, tests/,
and any *.cfc/*.cfm/*.js/*.ts/*.py change). Trivia uses
`gh pr merge --auto --squash --delete-branch`; code uses
`gh pr merge --squash --delete-branch` (no --auto — caller has
already confirmed the FULL suite is green at this point, not just
the required-checks subset).
When in doubt, treat as code. The classifier is one-way conservative:
ANY single non-trivia file in the PR forces code tier.
Exit codes are picked for cron friendliness:
0 At least one PR was merged (or eligible in --check mode)
1 No PRs eligible — normal for a quiet cron tick, not an error
2 Usage error / missing dependency / fatal API failure
Per-gate diagnostics print on stderr when a gate fails, identifying
exactly which gate blocked the merge. Output is TTY-aware (colours
when interactive, plain when piped).
Verified locally:
- Against #2368 (already merged) — short-circuits at Gate 1 with
"state=MERGED (need OPEN)", exits 1.
- --all --check with no open PRs — prints "no open PRs", exits 1.
- Against #99999 (does not exist) — graceful "failed to fetch PR"
diagnosis, exits 1.
Follow-up work (not in this PR):
- Register a recurring scheduled task that calls
`tools/auto-merge.sh --all` every 10–15 minutes and reports
merges. The script's exit codes and quiet output when nothing
matches make this trivial to wire up.
- Optional --json output mode for richer cron consumption.
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
bpamiri
added a commit
that referenced
this pull request
Apr 29, 2026
…2370) Adds tools/auto-merge.sh, a self-contained bash script that evaluates a PR against the project author's standing merge authorization rules and merges when every gate passes. Replaces the prior "agent reads CLAUDE.local.md prose, interprets gates by hand, decides whether to merge" workflow with a deterministic shell predicate that can be wired into a scheduled task / cron loop without further human review. Designed for two surfaces: 1. Interactive: `tools/auto-merge.sh <pr>` evaluates one PR and merges if eligible. `--check` makes it print-only. 2. Unattended: `tools/auto-merge.sh --all --check` lists the current user's open PRs, prints per-gate diagnostics, exits 1 when nothing is eligible. Drop the `--check` to make it merge for real. Suitable for a 10-minute cron tick. Eight gates, evaluated in fail-fast order: 1. PR is OPEN 2. PR author == current `gh api user .login` (refuse to merge other people's PRs even if they share the org) 3. baseRefName == develop (NEVER main / master / feature branches) 4. mergeable == MERGEABLE (no merge conflicts) 5. mergeStateStatus == CLEAN (branch up to date with base, full check suite green, no required reviews outstanding) 6. reviewRequests is empty (no reviewer pending a verdict) 7. reviewDecision != CHANGES_REQUESTED 8. Every check in statusCheckRollup is conclusion=SUCCESS — no FAILURE / IN_PROGRESS / PENDING / QUEUED / CANCELLED When all eight pass, the script classifies the PR into trivia tier (every changed file is top-level *.md, .gitignore, .gitattributes, or .editorconfig) or code tier (anything else, including docs/, tests/, and any *.cfc/*.cfm/*.js/*.ts/*.py change). Trivia uses `gh pr merge --auto --squash --delete-branch`; code uses `gh pr merge --squash --delete-branch` (no --auto — caller has already confirmed the FULL suite is green at this point, not just the required-checks subset). When in doubt, treat as code. The classifier is one-way conservative: ANY single non-trivia file in the PR forces code tier. Exit codes are picked for cron friendliness: 0 At least one PR was merged (or eligible in --check mode) 1 No PRs eligible — normal for a quiet cron tick, not an error 2 Usage error / missing dependency / fatal API failure Per-gate diagnostics print on stderr when a gate fails, identifying exactly which gate blocked the merge. Output is TTY-aware (colours when interactive, plain when piped). Verified locally: - Against #2368 (already merged) — short-circuits at Gate 1 with "state=MERGED (need OPEN)", exits 1. - --all --check with no open PRs — prints "no open PRs", exits 1. - Against #99999 (does not exist) — graceful "failed to fetch PR" diagnosis, exits 1. Follow-up work (not in this PR): - Register a recurring scheduled task that calls `tools/auto-merge.sh --all` every 10–15 minutes and reports merges. The script's exit codes and quiet output when nothing matches make this trivial to wire up. - Optional --json output mode for richer cron consumption. Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2 tasks
bpamiri
added a commit
that referenced
this pull request
Apr 29, 2026
#2372) PR #2369 made simple.css the actual default styling and corrected chapter 1's misleading "Basecoat arrives later" Aside. Three other places in the tutorial still claimed basecoat is part of the main flow even though chapters 2-7 never install or use it. This PR reconciles those references with what the tutorial actually delivers. Changes (3 files, 6 lines): - tutorial/index.mdx * description: drop "Basecoat" from the intro blurb * tech-stack list: replace "Styling: Basecoat UI ... arrives via the basecoat package" with the truth — simple.css is the default in the generated layout, basecoat is an optional post-tutorial upgrade. Also reframe the Hotwire bullet to match how Part 4 actually wires Turbo (CDN script tag, not the hotwire package — that's still aspirational at 4.0). * Part 3 row: change "Activate Hotwire and Basecoat" to "Tour the generated files and meet Wheels' package system". Chapter 3 actually does the latter; it never activates either package. - tutorial/02-first-model.mdx * "What's next" block: replace the false promise that Part 3 will activate Hotwire and Basecoat with what Part 3 really does — tour the package system + first look at Hotwire ahead of Part 4 where Turbo Frames land for real. - tutorial/07-testing-deploying.mdx * "What you built" recap: replace "Full CRUD scaffold with Basecoat styling" with "Full CRUD scaffold rendered with simple.css default styling and Turbo Drive page transitions". The reader did not, in fact, build with Basecoat. Chapter 3's package catalog (which lists hotwire, basecoat, sentry as examples) stays unchanged — that section describes the package system generically and is factually correct. Chapter 1's Aside (added in PR #2369) is the canonical word on the styling default. A bonus chapter walking through `wheels packages install wheels-basecoat` is planned as a follow-up PR. Today's runtime can't install the package end-to-end (the BuildInfo version-detection bug fixed in PR #2368 is also breaking the package wheelsVersion compatibility check, and the `wheels packages install` subcommand has a separate routing issue that drops to LuCLI's installer instead of the Wheels module's). Once #2368 propagates to a brew snapshot and the install path is verified end-to-end, the bonus chapter follows. Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
3 tasks
bpamiri
added a commit
that referenced
this pull request
Apr 29, 2026
…ot unreachable wheels.Global (#2373) `PackagesMainCli.$detectRuntime()` tried to instantiate `wheels.Global` and call `$readFrameworkVersion()` on it. In the LuCLI command-line context the only registered framework mapping is `modules.wheels.*` — `wheels.Global` doesn't resolve, so every CLI invocation fell through the try/catch and returned the `0.0.0-dev` sentinel. The visible symptom: every `wheels packages` command reported the runtime as `0.0.0-dev`, regardless of the actual installed version. That made the package system version-blind from the CLI surface — `wheels packages show <name>` listed every released package as out-of-range for every published `wheelsVersion` constraint, e.g. $ wheels packages show wheels-basecoat ... Compatible versions (runtime 0.0.0-dev): (none — this runtime is out of range for every published version) …even when the brew-installed runtime was 4.0.0-SNAPSHOT+1644 and wheels-basecoat 1.0.1 advertises `wheelsVersion: ">=4.0"`. Fix: replace the single try/catch with a three-tier fallback chain: 1. Read .module-version text file (brew/chocolatey installs write this at install time). Plain text — no CFML compilation, no mapping lookup, and immune to BuildInfo.cfc bugs like the self-substituting-sentinel issue fixed in #2368. 2. Instantiate the bundled BuildInfo.cfc directly via the `modules.wheels.*` mapping that LuCLI guarantees. Covers ForgeBox installs and dev checkouts where .module-version isn't written. 3. Sentinel `0.0.0-dev` — matches "*" against any wheelsVersion constraint via the SemVer comparator (permissive dev build). Tier 1 is the fast path on every brew/chocolatey install. Tier 2 is the right answer for source checkouts. Tier 3 keeps the "everything matches" behaviour of the previous catch block as the last-resort fallback. Verified end-to-end on a fresh VM running 4.0.0-SNAPSHOT+1644: Before: $ wheels packages show wheels-basecoat Compatible versions (runtime 0.0.0-dev): (none — this runtime is out of range for every published version) After (with this fix scp'd to the VM's installed module): $ wheels packages show wheels-basecoat Compatible versions (runtime 4.0.0-SNAPSHOT+1644): 1.0.1 [wheelsVersion >=4.0] published 2026-04-24T00:00:00Z 1.0.0 [wheelsVersion >=4.0] published 2026-04-23T00:00:00Z Existing PackagesMainCliSpec tests inject `runtimeVersion` directly, which is why the `wheels.Global` lookup-failure was never exercised under test. Fixing the mock-vs-real gap is left as follow-up — the right approach is probably integration-style tests against a sandbox-installed module rather than further unit-level injection. This PR is a prerequisite for the eventual wheels-basecoat tutorial bonus chapter — readers cannot install packages until the CLI knows its own runtime version. Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This was referenced Apr 29, 2026
Merged
Merged
bpamiri
added a commit
to wheels-dev/wheels-hotwire
that referenced
this pull request
Apr 29, 2026
Same fix as wheels-dev/wheels-basecoat#2 — the component declaration component mixin="controller,view" output="false" asks Lucee 7 to load `view.cfc` as a trait via native mixin composition. There is no `view.cfc` on the component path (Wheels has no `view` mixin target — view helpers go into `controller` mixins because views render in the controller's variables scope). The missing trait makes the whole component fail to compile, with a misleading "can't find component [vendor.wheels-hotwire.Hotwire]" error that points at the outer component rather than the unresolved trait. Net effect: wheels-hotwire silently fails to activate on Lucee 7. No Turbo Drive, no Turbo Frames, no Turbo Streams, no Stimulus controllers. Package shows up in `application.wheels.failedPackages`. Fix: drop `view` from the mixin attribute. `package.json`'s `provides.mixins: "controller"` field is the actual source of truth for the framework's PackageLoader. The component-level attribute is a historical convention now obsolete on Lucee 7. Lucee 5/6 don't enforce native mixin composition the same way, which is why this went undetected until Lucee 7 became the default in Wheels 4.0. Pairs with wheels-dev/wheels#2368 + #2373 + #2374 (framework-side fixes that get the install path working) and wheels-dev/wheels-basecoat#2 (same fix in the basecoat package). Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
bpamiri
added a commit
that referenced
this pull request
Apr 29, 2026
Adds 08-bonus-basecoat.mdx, a 30-minute optional follow-up to Part 7
that walks through installing the wheels-basecoat package and
rewriting the post show view using uiCard, uiField, and uiButton
helpers. Lands the Wheels package system as a teachable end-to-end
flow rather than a chapter-3 conceptual aside.
Chapter shape follows the existing tutorial conventions:
- "Where we left off" recap so readers can resume from a clean
Part-7 state.
- "Why basecoat over simple.css" frames the choice as a tradeoff,
not a recommendation. Tutorial readers stay on simple.css; the
chapter is for when you've finished the tutorial and want a real
component kit.
- Steps blocks for install, CSS asset serving, layout wiring, and
view rewrite.
- Checkpoint with three concrete `curl | grep` verifications a
reader can run themselves.
- Troubleshooting with four real failure modes I hit during
end-to-end verification on a fresh VM, including the version-
detection edge case (`No version of 'wheels-basecoat' satisfies
runtime '0.0.0-dev'`) tied to PR #2373.
The install path uses `wheels packages add wheels-basecoat` (the
canonical verb after PR #2374), not `install`. The chapter explicitly
calls out the LuCLI interception with a caution Aside so readers
who reach for the historic verb get an immediate explanation.
Adjusts:
- tutorial/index.mdx — adds the bonus chapter as a row in the
parts table and as a card in the "Ready to start" CardGrid.
- 01-hello-wheels.mdx — the existing "On styling" Aside now links
to the bonus chapter for upgrade-path readers (was a bare GitHub
repo link).
- 07-testing-deploying.mdx — adds the bonus chapter as the
first card in "What to read next" (recommended next step
immediately after finishing the main tutorial).
Prerequisites for the chapter to actually work end-to-end:
- PR #2368: BuildInfo.isDev() self-substituting sentinel fix
(merged) — needed for runtime version reporting.
- PR #2373: $detectRuntime fix (merged) — CLI knows its runtime
version.
- PR #2374: `wheels packages install` → `add` rename (merged) —
canonical install command works.
- wheels-dev/wheels-basecoat#2: drop `view` from the mixin
component attribute (open) — required for Lucee 7 helper
activation. Tutorial reader's experience depends on basecoat
1.0.2 (or whatever ships this fix) being current in the
registry.
- wheels-dev/wheels-hotwire#2: same fix on the hotwire side.
Verified end-to-end on a fresh VM: with all five fixes in place,
`wheels packages add wheels-basecoat` followed by a full server
restart produces working `#uiButton(...)#`, `#uiCard(...)#`,
`#uiField(...)#` calls in views. The rendered HTML is shadcn/ui-
quality output.
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
3 tasks
bpamiri
added a commit
that referenced
this pull request
Apr 29, 2026
Adds 08-bonus-basecoat.mdx, a 30-minute optional follow-up to Part 7
that walks through installing the wheels-basecoat package and
rewriting the post show view using uiCard, uiField, and uiButton
helpers. Lands the Wheels package system as a teachable end-to-end
flow rather than a chapter-3 conceptual aside.
Chapter shape follows the existing tutorial conventions:
- "Where we left off" recap so readers can resume from a clean
Part-7 state.
- "Why basecoat over simple.css" frames the choice as a tradeoff,
not a recommendation. Tutorial readers stay on simple.css; the
chapter is for when you've finished the tutorial and want a real
component kit.
- Steps blocks for install, CSS asset serving, layout wiring, and
view rewrite.
- Checkpoint with three concrete `curl | grep` verifications a
reader can run themselves.
- Troubleshooting with four real failure modes I hit during
end-to-end verification on a fresh VM, including the version-
detection edge case (`No version of 'wheels-basecoat' satisfies
runtime '0.0.0-dev'`) tied to PR #2373.
The install path uses `wheels packages add wheels-basecoat` (the
canonical verb after PR #2374), not `install`. The chapter explicitly
calls out the LuCLI interception with a caution Aside so readers
who reach for the historic verb get an immediate explanation.
Adjusts:
- tutorial/index.mdx — adds the bonus chapter as a row in the
parts table and as a card in the "Ready to start" CardGrid.
- 01-hello-wheels.mdx — the existing "On styling" Aside now links
to the bonus chapter for upgrade-path readers (was a bare GitHub
repo link).
- 07-testing-deploying.mdx — adds the bonus chapter as the
first card in "What to read next" (recommended next step
immediately after finishing the main tutorial).
Prerequisites for the chapter to actually work end-to-end:
- PR #2368: BuildInfo.isDev() self-substituting sentinel fix
(merged) — needed for runtime version reporting.
- PR #2373: $detectRuntime fix (merged) — CLI knows its runtime
version.
- PR #2374: `wheels packages install` → `add` rename (merged) —
canonical install command works.
- wheels-dev/wheels-basecoat#2: drop `view` from the mixin
component attribute (open) — required for Lucee 7 helper
activation. Tutorial reader's experience depends on basecoat
1.0.2 (or whatever ships this fix) being current in the
registry.
- wheels-dev/wheels-hotwire#2: same fix on the hotwire side.
Verified end-to-end on a fresh VM: with all five fixes in place,
`wheels packages add wheels-basecoat` followed by a full server
restart produces working `#uiButton(...)#`, `#uiCard(...)#`,
`#uiField(...)#` calls in views. The rendered HTML is shadcn/ui-
quality output.
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
Every snapshot since #2352 has been silently reporting itself as
0.0.0-devin the debug bar,wheels infooutput, and anywhereapplication.wheels.versionflows. Surfaced by a fresh-VM tutorial run on4.0.0-SNAPSHOT+1644where the user expectedWheels 4.0.0-SNAPSHOT+1644in the debug bar but sawWheels 0.0.0-dev.Root cause
tools/build/scripts/prepare-core.shdoes a globalsed s/@build.version@/<version>/goverBuildInfo.cfcat artifact-construction time. Before this fix,isDev()comparedvariables.info.versionto the literal"@build.version@"string:public boolean function isDev() { return variables.info.version == "@build.version@"; // literal sentinel }The sed pass rewrote the sentinel-literal too, turning the comparison into:
…which is always true on every released build, since
variables.info.versionwas substituted to the same value. Thenversion()doesisDev() ? "0.0.0-dev" : variables.info.version→ always returns"0.0.0-dev".This is a textbook self-substituting sentinel: the in-source value used for "this hasn't been rewritten yet" detection looks identical to the value being rewritten.
Fix
Replace the literal-equality check with the structural prefix/suffix check that
$blankIfPlaceholder()already uses in the same file:public boolean function isDev() { var v = variables.info.version; return left(v, 7) == "@build." && right(v, 1) == "@"; }The matcher never embeds the full sentinel string
@build.version@, so it's invariant under any single-token substitution.Regression guard
Adds a structural test in
buildInfoSpec.cfcthat reads the source and asserts the literal@build.version@appears exactly once (only in thevariables.info.versionfield). A second occurrence anywhere — comments, comparison logic — is rewritten by the release pipeline and re-introduces the bug. This test would have caught the issue when #2352 first introduced BuildInfo.cfc.The pre-existing tests run against the source (placeholder still literal), where the buggy
isDev()happens to return the right answer; only post-substitution artifacts trigger the bug, which is why this slipped through.Verification
Verified end-to-end on a fresh-VM blog app running brew-installed
4.0.0-SNAPSHOT+1644:application.wheels.buildInfo.isDev()truefalseapplication.wheels.buildInfo.version()"0.0.0-dev""4.0.0-SNAPSHOT+1644"Wheels 0.0.0-devWheels 4.0.0-SNAPSHOT+1644Reproduced by sed-substituting the fixed source locally (
sed 's/@build\.version@/4.0.0-SNAPSHOT+1644/g'), scp'ing the result to the VM'sblog/vendor/wheels/BuildInfo.cfc, restarting the server, and inspecting the rendered debug bar — exactly the artifact shape the release pipeline produces.Unit-level verification via direct CFM probe:
Test plan
wheels inforeports the concrete build versionBuildInfo.cfcinwheels-core-<v>.zipartifact still has exactly one@build.version@literal pre-substitution and zero post-substitutionRelated
0.0.0-devdisplay gap on installed apps; this is a regression of that fix that landed silently when the version source moved from box.json to BuildInfo.cfc)