fix(ci): stop double-nesting framework inside Linux .deb/.rpm packages#2776
Conversation
The nfpm contents rule pointed `src` at `./build/framework/` for the
framework staging step. `wheels-core-VER.zip` carries a top-level
`wheels/` directory that `unzip` preserves, so the resulting tree was
`./build/framework/wheels/...`. nfpm `type: tree` copies *contents* of
src into dst, which meant the inner `wheels/` wrapper itself landed at
the destination — producing
`/opt/wheels/module/vendor/wheels/wheels/Injector.cfc` instead of
`/opt/wheels/module/vendor/wheels/Injector.cfc`.
After the user-side wrapper sync (`/opt/wheels/module/*` →
`~/.wheels/modules/wheels/*`) and `wheels new <app>` copy, every fresh
Linux install ended up with the framework one directory level too deep.
Lucee's `/wheels` mapping pointed at the (empty) outer directory, so
`new wheels.Injector("wheels.Bindings")` in the generated
`public/Application.cfc` threw `could not find component or class with
name [wheels.Injector]` on the first request. The existing onError
handler then dereferenced `application.wo` (which was never assigned
because Injector init failed), surfacing only the cryptic cascade
`The key [WO] does not exist.` — issue #2773.
The brew formula handles this correctly by re-introducing the wheels/
wrapper at stage time
(`(share/"wheels/framework/wheels").install Dir["*"]`). Both Linux nfpm
configs now pin `src` at `./build/framework/wheels/` so the contents
flatten into `/opt/wheels/module/vendor/wheels/` as intended.
The published 4.0.1 .deb / .rpm artifacts ship the broken layout (1
.deb download, 0 .rpm at time of fix). A re-released 4.0.2 will be
needed to deliver the fix to users — the change here is to the build
config only, not to any framework or CLI code.
Tests: `vendor/wheels/tests/specs/cli/LinuxPackageStagingSpec.cfc`
gains a per-channel `it()` that asserts `src: ./build/framework/wheels/`
+ `dst: /opt/wheels/module/vendor/wheels/` are paired in each nfpm
yaml. Structural assertion follows the existing #2700 pattern (the
file already pins four other packaging invariants the same way).
Note on local verification: the structural spec was sanity-checked via
equivalent grep / perl POSIX patterns over the YAMLs (positive match
for the fixed form, zero matches for the buggy form). Running the
spec through the CFML runner locally was blocked by a port-8081
collision with two stale wheels server processes from prior dev
sessions — CI compat-matrix will run the spec across every engine ×
DB on this PR.
Closes #2773
Signed-off-by: Peter Amiri <peter@alurium.com>
There was a problem hiding this comment.
Wheels Bot — Reviewer A
TL;DR: This PR correctly fixes a build-config regression where both Linux .deb / .rpm packages double-nested the framework one directory level too deep, causing every fresh Linux install to crash on first request. The change is surgical (two one-line YAML edits), well-documented, and includes a regression spec. Verdict: comment -- only minor nits, nothing blocking.
Correctness
The root cause is accurately diagnosed and correctly fixed. The nfpm type: tree semantics copy the contents of src into dst. Because wheels-core-VER.zip unpacks with a top-level wheels/ directory, pointing src at ./build/framework/ caused the inner wheels/ directory itself to appear at the destination, producing vendor/wheels/wheels/Injector.cfc instead of vendor/wheels/Injector.cfc. Pinning src to ./build/framework/wheels/ is the right fix and mirrors the brew formula's explicit (share/"wheels/framework/wheels").install Dir["*"].
The fix is applied symmetrically to both nfpm-wheels.yaml and nfpm-wheels-be.yaml, and the CHANGELOG and spec are consistent with the change.
Tests
Nit 1 -- missing negative assertion (vendor/wheels/tests/specs/cli/LinuxPackageStagingSpec.cfc, new it() at line 167):
The new block only asserts the correct path is present:
var pair = reFindNoCase(
"src:[[:space:]]+\./build/framework/wheels/[[:space:]]+dst:[[:space:]]+/opt/wheels/module/vendor/wheels/",
src
) > 0;
expect(pair).toBeTrue(...);The rest of the file follows a dual-assertion pattern. Lines 60-68, for example, pair a toBeFalse check for the wrong form with a toBeTrue check for the right one. A corresponding negative check would be consistent and guard against a future copy-paste accident where both the old and new src paths coexist:
var hasBuggyPath = reFindNoCase(
"src:[[:space:]]+\./build/framework/[[:space:]]+dst:[[:space:]]+/opt/wheels/module/vendor/wheels/",
src
) > 0;
expect(hasBuggyPath).toBeFalse(
t.label & " must NOT declare `src: ./build/framework/` (without the trailing /wheels/). "
& "If both entries coexist, nfpm stages the inner wheels/ wrapper as a subdirectory, "
& "double-nesting the framework. See issue ##2773."
);Nit 2 -- cross-line regex not verified locally (LinuxPackageStagingSpec.cfc:183):
The regex spans a YAML line boundary. The [[:space:]]+ between ./build/framework/wheels/ and dst: must match \n (newline + indent). CFML's reFindNoCase maps [[:space:]] to Java's \s which does include \n, so this is theoretically correct. The PR checklist acknowledges the spec was not run through the CFML runner locally due to a port conflict. CI will be the first actual verification.
Commits
Nit 3 -- fix(ci): vs ci: for build-config changes:
CLAUDE.md notes "ci is a TYPE, not a scope -- never write refactor(ci):." The spirit is that CI/build-config changes should use ci as the type. commitlint.config.js is the authoritative source and allows any scope (unrestricted), so this commit passes the linter. But per the CLAUDE.md guidance, ci: stop double-nesting framework inside Linux .deb/.rpm packages would be the preferred form. Minor observation only -- does not block merge.
Docs
CHANGELOG entry under [Unreleased] -> Fixed is present and accurate. The bullet is verbose but captures the full chain of causation, consistent with the 4.0.1 entries already in the file.
Security
No concerns. Build-config YAML and a structural test spec only. No secrets, no user-input paths, no framework code touched.
Wheels Bot — Reviewer B (round 1)A's review is substantively sound — the correctness analysis is accurate and the three nits are reasonable observations. One false positive to correct: A's characterization of Nit 1 overstates the inconsistency. After correcting that framing the underlying suggestion still holds, and I'm aligned on a "comment" verdict (nothing blocking, one addressable improvement). Emitting SycophancyNone detected. A's opening "This PR correctly fixes..." is backed by a detailed explanation of nfpm False positivesNit 1 framing is inaccurate. A claims: "The rest of the file follows a dual-assertion pattern. Lines 60-68, for example, pair a That pattern is present in the build-script section ( The underlying Nit 1 suggestion (add a negative assertion) is still a useful defensive improvement — if both the buggy and fixed No other false positives. Nit 2 (cross-line regex theory) and Nit 3 (commit type convention) are accurate observations. Missed issuesNone detected. The diff touches only Verdict alignmentA's "comment" verdict is consistent with the findings — three nits, nothing blocking. Correct call. ConvergenceAligned. After correcting Nit 1's framing, the underlying finding (add a negative assertion to |
Reviewer A on PR #2776 (wheels-bot) flagged that the new framework-src spec only asserted the *fixed* form was present, without a matching `toBeFalse` for the buggy `./build/framework/` form. The file's existing wrapper-routing checks (lines 60-68 / 81-106) already use a dual- assertion pattern; the new spec was a one-sided outlier. Add the negative guard: if a future copy-paste leaves both the bare `src: ./build/framework/` and the fixed `src: ./build/framework/wheels/` in the same yaml, nfpm would stage both — the bare one reintroduces the double-nesting and breaks every fresh Linux install. The spec now fails loudly in that scenario instead of silently passing on the presence of the fixed entry. The two regexes are mutually exclusive by construction: the positive matches `framework/wheels/` followed by whitespace + `dst:`; the negative matches `framework/` followed *immediately* by whitespace + `dst:`. Since `wheels` isn't whitespace, `[[:space:]]+` can't bridge across it, so the negative regex cannot false-positive on the fixed form. Confirmed via perl POSIX equivalent against both nfpm yamls plus a synthetic buggy fixture. Also adds an inline comment to the positive assertion documenting why `[[:space:]]+` works across the YAML line break (POSIX `[[:space:]]` resolves to Java's `\s` in both Lucee and Adobe CF, which includes `\n`) — addresses Reviewer A's Nit 2 observation that the cross-line match hadn't been locally verified. Signed-off-by: Peter Amiri <peter@alurium.com>
Thanks for the review. Addressed in
|
Wheels Bot — Address Review (round 1)Applied consensus findings from Reviewer A and Reviewer B's
Skipped (intentionally — not in the consensus action set):
Test verification: local re-run via The new commit will trigger a fresh Reviewer A run on the updated |
Acknowledged. Both findings already landed in
PR should be merge-ready once CI greens on |
There was a problem hiding this comment.
Wheels Bot — Reviewer A
TL;DR: PR #2776 fixes the double-nested framework path in Linux .deb / .rpm packages that broke every fresh install with could not find component or class with name [wheels.Injector]. The two-line YAML fix is correct, the comments are thorough, and the new commit 5534cbb3f fully addresses both nits from round 1 (negative guard added, cross-line regex behaviour documented). No correctness, cross-engine, or security issues remain. Verdict: approve.
Round 1 nits — resolved
Nit 1 (missing negative assertion) — addressed by 5534cbb3f. The hasBuggyPair / toBeFalse block now follows the dual-assertion pattern already used at lines 60-68 and 81-106 of LinuxPackageStagingSpec.cfc. The two regexes are mutually exclusive by construction: the positive anchors on framework/wheels/ and the negative anchors on framework/ followed immediately by [[:space:]]+dst:; since wheels is not whitespace, the negative cannot false-positive on the fixed form.
Nit 2 (cross-line regex not locally verified) — addressed by 5534cbb3f. The commit message records that the mutual-exclusion property was confirmed via a Perl POSIX equivalent against both YAMLs plus a synthetic buggy fixture, and an inline comment was added to the spec explaining that POSIX [[:space:]] resolves to Java's \s (which includes \n) in both Lucee and Adobe CF.
Commits
Both commits pass commitlint. The same minor style note from round 1 still applies: CLAUDE.md says ci is a type, not a scope, so fix(ci): and test(ci): are mildly unconventional (preferred forms would be ci: and test:). The linter is the authoritative source and accepts both — not blocking.
Both commits carry Signed-off-by: Peter Amiri <peter@alurium.com>. DCO is clean.
Correctness
The core fix is correct. nfpm's type: tree semantics copy the contents of src into dst, so pointing src at ./build/framework/wheels/ correctly flattens Injector.cfc into /opt/wheels/module/vendor/wheels/ rather than .../vendor/wheels/wheels/. The fix is applied symmetrically to both nfpm-wheels.yaml and nfpm-wheels-be.yaml and mirrors the brew formula's (share/"wheels/framework/wheels").install Dir["*"].
The nfpm-wheels-be.yaml comment delegates to nfpm-wheels.yaml for the full rationale — a reasonable DRY choice given the two files move in lock-step.
Tests
LinuxPackageStagingSpec.cfc now contains both the positive (hasFixedPair / toBeTrue) and negative (hasBuggyPair / toBeFalse) assertions, running over both YAML files via the nfpmTargets loop. The spec has not been run through the CFML runner locally (port conflict noted in the checklist) but the CI compat-matrix will exercise it across all engines x DBs on this PR — the same posture as round 1 and acceptable given the structural nature of the test.
Docs
CHANGELOG [Unreleased] -> Fixed entry is present, accurate, and cross-references issue #2773. No user-facing framework behaviour changed, so guide updates are correctly left unchecked.
Security
No concerns. Build-config YAML and a structural test spec only. No user input, no secrets, no framework code touched.
|
updated content test |
…sting Resolves CHANGELOG.md conflict in the [Unreleased] / Fixed section: both this branch and #2774 (just merged to develop) added a new [Unreleased] block at the same anchor. Kept develop's H1 header form (`# [Unreleased]`, matching the 4.0.1 release header style) and merged both fix bullets under a single ### Fixed list — the .deb/.rpm framework nesting fix from this branch first (root cause), then #2774's defensive onError guard second (defensive belt for the same #2773 cascade). Both fixes close #2773 from different angles, so listing them together is accurate. Signed-off-by: Peter Amiri <peter@alurium.com>
There was a problem hiding this comment.
Wheels Bot — Reviewer A
TL;DR: Round 3 re-review after 9dd71c6d3 merges develop into the branch. The PR-specific changes — the src path correction in both nfpm YAMLs and the dual-assertion spec block — are byte-for-byte identical to what was approved in round 2. The merge commit's only substantive work is a CHANGELOG conflict resolution: the two [Unreleased] → Fixed bullets (this PR's nfpm fix + #2774's application.wo guard) are now correctly listed together under a single ### Fixed header. No new correctness, cross-engine, or security concerns. Verdict: approve.
Correctness
The core fix remains correct and unchanged. nfpm's type: tree semantics copy the contents of src into dst. Because wheels-core-VER.zip preserves a top-level wheels/ directory on unzip, pointing src at ./build/framework/ staged the inner wheels/ directory itself into /opt/wheels/module/vendor/wheels/, producing .../vendor/wheels/wheels/Injector.cfc. Pinning src to ./build/framework/wheels/ (the inner directory) passes nfpm the unpacked contents directly, matching the brew formula's (share/"wheels/framework/wheels").install Dir["*"] approach. Applied symmetrically to both nfpm-wheels.yaml and nfpm-wheels-be.yaml.
Merge commit (9dd71c6d3)
The merge touches eight files; none are PR-specific (the nfpm YAMLs and LinuxPackageStagingSpec.cfc are untouched). The develop side brought in:
CHANGELOG.md— conflict resolved by retaining develop's# [Unreleased]H1 header form and listing both fix bullets sequentially. Accurate: both this PR (root cause) and #2774 (defensive guard) close #2773 from different angles.public/Application.cfc,cli/lucli/templates/app/public/Application.cfc,OnErrorFallbackGuardSpec.cfc, docs — all from #2774, already merged to develop and reviewed there.
The conflict resolution is correct. No artifacts of the pre-merge state survive.
Tests
LinuxPackageStagingSpec.cfc contains both the positive (hasFixedPair / toBeTrue) and negative (hasBuggyPair / toBeFalse) assertions inside the nfpmTargets loop, covering both YAML files. The two regexes are mutually exclusive: the positive anchors on framework/wheels/ while the negative anchors on framework/ followed immediately by [[:space:]]+dst: — since wheels is not whitespace, the fixed path cannot satisfy the negative pattern. ##2773 in the error strings correctly escapes the CFML expression delimiter. The [[:space:]]+ spanning the YAML line break resolves to Java's \s (which includes \n) in both Lucee and Adobe CF, as documented in the inline comment at line 184.
Commits
Three commits on the branch. 49274ee and 5534cbb pass commitlint and carry Signed-off-by: Peter Amiri. The merge commit 9dd71c6d3 also carries Signed-off-by: Peter Amiri. DCO is clean across all three.
The prior round's style note still stands: CLAUDE.md notes ci is a type, not a scope, so fix(ci): and test(ci): are mildly unconventional (preferred: ci: and test:). Commitlint accepts both — not blocking.
Docs
CHANGELOG.md [Unreleased] → Fixed now carries both the nfpm path fix and the application.wo guard, cross-referencing #2773. No user-visible framework behaviour changed by the YAML fix, so guide updates are correctly left unchecked.
Security
No concerns. Build-config YAML, a structural spec, and a CHANGELOG edit only. No user input, no secrets, no framework code.
Wheels Bot — Reviewer B (round 1)A's round 3 re-review is clean. The correctness analysis is accurate, the regex mutual-exclusivity argument holds, and the approve verdict is fully supported by the evidence. No sycophancy, no false positives, no missed issues. SycophancyNone detected. A opens with a specific factual claim ("PR-specific changes are byte-for-byte identical to what was approved in round 2") and backs every section with line-level evidence. No unsubstantiated LGTM. False positivesNone detected.
Missed issuesNone detected. The diff is limited to two build-config YAML files, a structural CFML spec (pure Verdict alignmentA's approve is consistent with the findings. Three nits from prior rounds: Nit 1 (negative assertion) was addressed in ConvergenceAligned. A's approve is well-supported and I would reach the same verdict independently. PR is review-clean on this SHA pending CI green on |
…be_* (#2777) * docs(web/guides): correct Linux bleeding-edge install URLs to wheels-be_* PR #2759 (2026-05-18) renamed the snapshot Linux artifacts from `wheels_*` to `wheels-be_*` (debs) and `wheels-be-*.x86_64.rpm` (rpms) so the package name itself differentiates the channel. The install guides were not updated alongside that rename, so every documented `curl -fsSLO ...` command for Linux bleeding-edge install resolves to a 404 against the actual snapshot release assets. Verified against v4.0.2-snapshot.1923 (published 2026-05-20): Guide says: .../wheels_4.0.2.snapshot.1923_amd64.deb → 404 Actual asset: .../wheels-be_4.0.2.snapshot.1923_amd64.deb Fix all six pages where the snippets / prose examples appear (three unique pages mirrored across v4-0-0 and v4-0-1-snapshot doc versions): start-here/installing.mdx — "Want bleeding-edge?" aside start-here/release-channels.mdx — main BE install snippets + "Switching channels" snippets + tilde-mangling prose command-line-tools/installation.mdx — bleeding-edge install snippets The substitutions are scoped to bleeding-edge contexts (snippets using `${SNAP_FILENAME_VER}` and prose `wheels_4.0.0.snapshot.*` filename examples). Stable-channel snippets, which use `${WHEELS_VERSION}` and fetch from `wheels-dev/wheels` (not `wheels-snapshots`), are unchanged — they correctly retain the bare `wheels_` / `wheels-` prefixes because the stable package name on Linux is still just `wheels`. Without this fix, users cannot install or test bleeding-edge / develop snapshots on Linux via the documented flow. This blocks user-side verification of develop-only fixes before they ship in the next stable patch — including PR #2776 (Linux .deb framework nesting fix) and PR #2774 (defensive onError guard), both of which close issue #2773. Signed-off-by: Peter Amiri <peter@alurium.com> * docs(web/guides): fix release-channels.mdx — missed BE Tab URLs + Linux switching semantics Round-1 reviewer findings on PR #2777: A's Nit 1 — primary install Tabs at lines 104-105 (Debian/Ubuntu BE) and 112 (Fedora/RHEL BE) of `release-channels.mdx` still resolved to 404. My initial verification sweep grep'd for `${SNAP_FILENAME_VER}`, but these snippets bind the tag to `${WHEELS_FILENAME_VER}` (a different bash var name). The fix is the same — point at the `wheels-be_` / `wheels-be-` artifacts. A's Nit 2 + B's catch — the "Switching channels" section had three related staleness bugs after #2759 renamed the BE package: 1. Line 129 prose claimed "only a single package name (`wheels`) is published per channel today" — false post-rename. 2. Lines 142-143 inline comment ("upgrades in place — no uninstall step needed") was true when both channels shared the `wheels` name, but the new world depends on the actual nfpm-declared `Replaces:` / `Conflicts:` metadata. B caught the contradiction between A's proposed line-129 prose and the existing line-142 comment. 3. Lines 158-172 (Linux BE → stable, both Debian and Fedora) had the *same* conceptual bug as 142-143: they prescribed `--allow-downgrades` (apt) / `dnf downgrade`, both of which assume same-package-name version transitions. With different names, both would fail with a `/usr/bin/wheels` file conflict because the stable `wheels` package doesn't declare `Replaces:`/`Obsoletes: wheels-be`. Reviewers didn't explicitly flag this set, but it's the same root cause and listing them inconsistently would have left readers worse off. Verified the actual nfpm metadata before rewriting (so the prose matches what the packages really declare): wheels-be deb: Replaces: wheels + Conflicts: wheels wheels-be rpm: Conflicts: wheels (no Obsoletes) wheels deb: no Replaces/Conflicts against wheels-be wheels rpm: no Conflicts/Obsoletes against wheels-be The new prose at line 129 explains the asymmetry up front; each snippet now carries a short comment naming the specific metadata that drives its action (or the lack of metadata that requires the explicit `apt remove` / `dnf remove`). Stable-channel snippets and stable install Tabs are unchanged. Signed-off-by: Peter Amiri <peter@alurium.com> * docs(web/guides): name the actual Conflicts declaration in BE→stable comments Reviewer A round-2 nit on PR #2777: the BE → stable (Debian) snippet's comment said apt "would fail with a /usr/bin/wheels file conflict", framing the failure mode as a dpkg-level file-ownership conflict. The actual blocker is the package-level `Conflicts: wheels` declaration in wheels-be's deb metadata — apt refuses the install with a package conflict error before dpkg ever attempts to unpack files. An advanced user debugging the actual error message would be confused by the file-conflict framing. Rewrite the Debian comment per A's suggestion, naming the actual mechanism: `wheels-be declares Conflicts: wheels`. Kept the secondary note about the missing `Replaces: wheels-be` in stable since it explains why apt also wouldn't auto-remove (relevant context if a reader wonders whether a single command could swap them). Updated the Fedora BE → stable comment to use parallel framing for consistency — same root cause (`wheels-be` declares `Conflicts: wheels`, applies bidirectionally on rpm too). Reviewer A only flagged the Debian site explicitly, but leaving the two comments inconsistent would have invited the same "two sites must agree" finding that caught round 1's line-142 / line-129 contradiction. Signed-off-by: Peter Amiri <peter@alurium.com> --------- Signed-off-by: Peter Amiri <peter@alurium.com>
Summary
Fixes the root cause behind #2773. The user's
Fresh installation of Wheels 4.0.1 → wheels new hello → wheels start → The key [WO] does not exist.cascade traces back to a one-line packaging bug: the .deb / .rpm contents rule unzippedwheels-core-VER.zip(which carries a top-levelwheels/directory) into./build/framework/, then pointed nfpmtype: treeat the outer directory. Result on disk afterdpkg -X/rpm2cpio:The wrapper-side sync (
/opt/wheels/module/*→~/.wheels/modules/wheels/*) and thewheels newframework copy both faithfully propagate the misnesting, so Lucee's generated/wheelsmapping points at an empty outer directory andnew wheels.Injector("wheels.Bindings")inpublic/Application.cfcthrowscould not find component or class with name [wheels.Injector]on the very first request. The cryptic[WO]error in the issue is the secondary cascade.The brew formula doesn't hit this because it explicitly re-introduces the wheels/ wrapper at stage time:
This PR brings the .deb / .rpm staging in line by pinning nfpm
srcat./build/framework/wheels/(the inner directory) in bothnfpm-wheels.yamlandnfpm-wheels-be.yaml. Comments in each file cross-link to the brew handling and to #2773 so the reasoning is preserved in-tree.I confirmed the bug against the actually-published GA artifacts (the .deb and .rpm currently sitting on the v4.0.1 release):
The change is build-config only — no framework / CLI / template code is modified, and the existing
wheels-core-VER.zip/wheels-module-VER.tar.gzartifact builders stay unchanged.Related Issue
Closes #2773
Type of Change
Feature Completeness Checklist
Signed-off-by:trailer presentit()block per channel invendor/wheels/tests/specs/cli/LinuxPackageStagingSpec.cfcasserts thesrc+dstpair lands at the correct flatten-friendly path. Follows the existing structural-assertion pattern from rpm packaging: v4.0.0 ships legacy wheels-cli module, missing .version / .channel files (titan cutover regression) #2700 (the file already pins four other packaging invariants the same way).bot-update-docs.ymlcan follow up if anything in the installing guide needs nuance.[Unreleased] → Fixedentry added above the 4.0.1 header.wheels serverprocesses from prior dev sessions — the CI compat-matrix will run the spec across every engine × DB on this PR.Test Plan
vendor/wheels/tests/specs/cli/LinuxPackageStagingSpec.cfcon every engine × DB combinationwheels_4.0.2_amd64.debandwheels-4.0.2.x86_64.rpmartifacts should placeInjector.cfcat/opt/wheels/module/vendor/wheels/Injector.cfc(not.../vendor/wheels/wheels/Injector.cfc). Quick verification:Release impact
The published v4.0.1 .deb / .rpm artifacts are baked-broken (the fix lives in build config, not in any framework/CLI code that's also in the artifact). Need to tag 4.0.2 to deliver the fix to Linux users —
apt install ./wheels_4.0.1_amd64.debwill continue to ship the broken layout until the new patch release is cut.Follow-ups (separate PRs)
application.woguard inonErrorso future init failures surface the underlying exception instead of the WO cascade. Worth landing alongside this fix; independent.tools/ci/smoke-test-module.shbypasses the nfpm-assembled layout viaWHEELS_FRAMEWORK_PATH, which is why this regression escaped. Worth adding a sibling smoke step that extracts the built .deb and asserts on the on-disk framework path — separate scope, separate PR.