fix(cli): ship lucli-native module, version stamps, and tar dep in Linux packages#2701
Conversation
…nux packages
The v4.0.0 .deb / .rpm Linux packages diverged from the brew formula in
three ways that broke `wheels start` on Rocky Linux during the titan
production cutover on 2026-05-13:
1. `build-linux-packages.sh` unzipped `wheels-cli-${WHEELS_VERSION}.zip`
(the legacy CommandBox-shaped artifact built from `cli/src/`) into
`/opt/wheels/module/` instead of untarring
`wheels-module-${WHEELS_VERSION}.tar.gz` (the lucli-native module built
from `cli/lucli/`, with `Module.cfc` at top). LuCLI couldn't resolve
any `wheels` verbs against the CommandBox-shaped module.
2. The wrapper ended with `exec /opt/wheels/lucli "$@"`, so basename(argv[0])
was `lucli` and LuCLI had no module context — `wheels start` became
`lucli start` and hit `Unknown command: 'start'`.
3. `nfpm-wheels.yaml` and `nfpm-wheels-be.yaml` never declared the
`.version` / `.channel` stamps under `contents:`, so `wheels --version`
returned `unknown (stable)` regardless of installed version.
Plus Rocky Linux 10 minimal cloud images don't ship `tar`, and any role
that unpacks the module payload fails silently without it.
This change:
- Stages `wheels-module-${WHEELS_VERSION}.tar.gz` into `/opt/wheels/module/`.
- Renames the LuCLI binary to `wheels` at stage time (mirroring the brew
formula's `libexec.install resource("lucli") => "wheels"`), so
`basename(argv[0])` is `wheels` and LuCLI's module dispatcher routes
through the bundled module.
- Adds `/opt/wheels/.version` and `/opt/wheels/.channel` to the nfpm
`contents:` block in both yamls.
- Adds `tar` to the rpm + deb runtime dependency list.
Regression test in `vendor/wheels/tests/specs/cli/LinuxPackageStagingSpec.cfc`
pins all four sub-fixes against the packaging file source.
Fixes #2700
Signed-off-by: claude[bot] <41898282+claude[bot]@users.noreply.github.com>
…nary rename The .deb/.rpm installation page described the package contents as including /opt/wheels/lucli (the LuCLI launcher). PR #2701 renamed the staged binary to /opt/wheels/wheels (mirroring the brew formula's argv[0]-based dispatch) and added .version/.channel stamp files. Update both v4-0-0 and v4-0-1-snapshot installation pages to reflect the corrected on-disk layout. Fixes #2700 Signed-off-by: wheels-bot[bot] <wheels-bot[bot]@users.noreply.github.com> Signed-off-by: claude[bot] <41898282+claude[bot]@users.noreply.github.com>
Wheels Bot — Docs updatedAdded a doc commit to this PR:
The installation guide described the package contents with the old |
There was a problem hiding this comment.
Wheels Bot — Reviewer A
TL;DR: This PR correctly fixes four v4.0.0 Linux packaging regressions (wrong module artifact, missing argv[0] rename, missing version/channel stamps, missing tar dep) that broke wheels start on Rocky Linux during the titan production cutover. The implementation changes are all correct and the CHANGELOG entry and DCO sign-off are present. Two minor test-quality nits, neither of which warrant blocking the merge — verdict: comment.
Correctness
The four sub-fixes are mechanically correct:
tar -xzf wheels-module-${WHEELS_VERSION}.tar.gzreplacingunzip wheels-cli-${WHEELS_VERSION}.zip— correct artifact, correct extraction tool.- Binary staged as
build/wheels(notbuild/lucli) sobasename(argv[0])iswheelswhen the wrapper execs it — mirrors the brew formula'slibexec.install resource("lucli") => "wheels". .version/.channelstamps declared undercontents:in both nfpm yamls — the old code wrote the files to staging but never added thecontents:entries, so nfpm silently dropped them.tardeclared as a runtime dep for bothrpmanddeb— defensive and correct even though the wrapper usescp -rat runtime, since downstream Ansible roles consuming the package may calltar.
One observation on the tar point: the runtime wrapper at build-linux-packages.sh:142 uses cp -r /opt/wheels/module/. "${MODULE_DIR}/" (not tar), so the installed package itself doesn't call tar at runtime. The PR body mentions "any role that unpacks /opt/wheels/module/" as the motivating case. That rationale is defensible, but a comment in the nfpm yaml would make the "why" clearer to future maintainers — the current comment in nfpm-wheels.yaml:76-78 mentions unpacking specifically, which is good.
Tests
Nit 1 — test title says "rpm" but regex is file-wide
vendor/wheels/tests/specs/cli/LinuxPackageStagingSpec.cfc:148:
it("declares tar as an rpm runtime dependency", () => {
var hasTarDep = reFindNoCase(
"-[[:space:]]+tar([[:space:]]|$)",
src
) > 0;The regex matches - tar anywhere in the file. In the current implementation tar appears in both the deb.depends and rpm.depends sections — so the test passes correctly. But if a future change removed tar from rpm.depends while leaving it in deb.depends, this test would falsely pass. The test title says "rpm runtime dependency" and should either (a) use a regex that anchors the check to the rpm: block, or (b) be renamed to "declares tar as an rpm and deb runtime dependency" so the scope matches the regex. Suggested anchor approach:
// Split the yaml at the rpm: overrides block and check only that segment
var rpmBlock = reFindNoCase("rpm:[^\n]+\n(.*)", src, 1, true);
// then check hasTarDep within rpmBlock.match[2]This is a test reliability concern, not a blocking correctness issue. The actual fix (both rpm and deb) is correct.
Nit 2 — nfpm binary path rename not covered
The spec verifies that build-linux-packages.sh uses exec /opt/wheels/wheels "$@" (not exec /opt/wheels/lucli "$@"), but there is no corresponding check that the nfpm yamls changed src: ./build/lucli → src: ./build/wheels and dst: /opt/wheels/lucli → dst: /opt/wheels/wheels. If the yaml binary path were inadvertently reverted while the build script remained fixed, nfpm would fail at package-build time with a "file not found" error — caught by CI — but the spec would still pass green. A one-liner reFindNoCase("dst:[[:space:]]+/opt/wheels/lucli", src) == 0 assertion on each yaml would close the gap.
Cross-engine
No CFML model/controller/view code is touched. The new spec uses the same expandPath("/wheels/../..") + IIFE-for-loop-closure pattern established in ConfigRoutesStaleDocUrlSpec.cfc (which ships and passes today), so there are no new cross-engine concerns.
Commits
Single commit edd39be74 — fix(cli): ship lucli-native module, version stamps, and tar dep in Linux packages. Type fix, subject under 100 chars, not ALL-CAPS. DCO sign-off present: Signed-off-by: claude[bot] <41898282+claude[bot]@users.noreply.github.com> matches the commit author identity. Conformant.
Docs
CHANGELOG entry under [Unreleased] → Fixed is present. Framework and AI reference docs deferred to bot-update-docs.yml — acceptable for a packaging-only fix with no framework API changes.
There was a problem hiding this comment.
Wheels Bot — Reviewer A
TL;DR: This PR fixes four v4.0.0 Linux packaging regressions (wrong module artifact, wrong binary name, missing version/channel stamps, missing tar dep) that broke wheels start on Rocky Linux. The implementation commit (edd39be74) was reviewed at the previous SHA; the new commit (15ba26138) is a clean docs follow-up that correctly syncs the installation pages with the binary rename. Two test-quality nits from the prior review cycle remain unaddressed, but neither is blocking. Verdict: comment.
Correctness
All four sub-fixes remain mechanically correct:
- Untarring
wheels-module-VERSION.tar.gzcorrectly stages the lucli-native artifact instead of the legacy CommandBox zip. - Binary staged as
build/wheelsand installed to/opt/wheels/wheels; wrapper ends withexec /opt/wheels/wheels "$@"so argv[0] dispatch works. /opt/wheels/.versionand/opt/wheels/.channeldeclared undercontents:in both nfpm yamls —wheels --versionno longer returnsunknown (stable).tardeclared inoverrides.rpm.dependsandoverrides.deb.dependsin both yamls.
The new docs commit (15ba26138) is factually accurate: the installation description now names /opt/wheels/wheels, /opt/wheels/.version, and /opt/wheels/.channel, matching the nfpm contents blocks exactly.
Tests
Two nits from the prior review cycle remain unaddressed. Neither blocks the merge, but both are worth a follow-up.
Nit 1 — tar-dep assertion is file-wide, not rpm-anchored
vendor/wheels/tests/specs/cli/LinuxPackageStagingSpec.cfc line 148 — the regex -[[:space:]]+tar([[:space:]]|$) matches - tar anywhere in the file. Since tar appears in both the deb.depends and rpm.depends sections, the test passes correctly today. But if a future change removed tar from only the rpm block while leaving it in deb, this test would false-pass while its title claims "rpm runtime dependency". Either anchor the check to the rpm section, or rename the test to "declares tar as an rpm and deb runtime dependency" to match the regex's actual scope.
Nit 2 — no nfpm binary-path assertion
The spec verifies that the build script wrapper uses exec /opt/wheels/wheels "$@" (not the old lucli form), but there is no corresponding check that the nfpm yamls changed dst: /opt/wheels/lucli to dst: /opt/wheels/wheels. If the yaml were inadvertently reverted, nfpm would fail at package-build time (caught by CI), but the spec would stay green. A one-liner per yaml would close the gap:
expect(reFindNoCase("dst:[[:space:]]+/opt/wheels/lucli", src)).toBe(0,
t.label & " must not reference the old /opt/wheels/lucli path");Docs
Both installation pages (v4-0-0 and v4-0-1-snapshot) are correctly updated. No other installation pages for these versions exist in the repo. The updated sentence correctly lists all five on-disk paths: /usr/bin/wheels, /opt/wheels/wheels, /opt/wheels/module/, /opt/wheels/.version + /opt/wheels/.channel, and /opt/wheels/sqlite-jdbc.jar.
Commits
Both commits conform to commitlint: type from the allowlist, subjects under 100 chars, not ALL-CAPS. edd39be74 carries one DCO sign-off line; 15ba26138 carries two (wheels-bot[bot] and claude[bot]) — DCO is satisfied by either.
Security
No CFML model/controller/view code changed. No secrets in the diff. No user-facing input paths introduced.
Wheels Bot — Reviewer B (round 1)A's review is solid. Both nits are real, all correctness claims check out, no sycophancy, no false positives. One inaccuracy in A's framing of Nit 2, but it does not change the verdict. Aligning on approve. SycophancyNone detected. A's TL;DR is backed by line-specific analysis for all four sub-fixes, and A did not approve — chose False positivesNone detected. I verified every claim A made against the diff:
One inaccuracy in A's Nit 2 framing: A writes the gap "would be caught by CI" as mitigation. That is not right. The PR body itself states end-to-end validation requires a Rocky/Debian sandbox and nfpm install — that is Missed issuesNone detected. The docs changes in the second commit ( Verdict alignmentA's ConvergenceAligned on approve. The four sub-fixes are correct, the CHANGELOG and DCO are in order, and the two nits A raised are real but non-blocking quality improvements to new test coverage. A's verdict is appropriate. The CI inaccuracy on Nit 2 is worth noting but does not change the overall recommendation. |
Wheels Bot — Reviewer B (round 1)A's review is accurate and consistent. No sycophancy, no false positives, and the two nits A flagged are real — but A explicitly characterized both as non-blocking follow-ups, not required changes. The implementation is correct on all four sub-fixes, and the tests do pin the regressions they claim to pin. I'm aligning on approve. SycophancyNone detected. A chose "comment" rather than "approve" precisely because the nits were unresolved — that is the more conservative call, not the easier one. False positivesNone detected. I re-read the diff against each of A's correctness claims:
Missed issuesNone that would change the verdict. One observation worth noting but not blocking: the spec constructs Verdict alignmentA's "comment" verdict is consistent with having two concrete but explicitly non-blocking nits. Either "comment" or "approve" would be defensible here; A's more conservative choice is reasonable, not misaligned. ConvergenceAligned. A's two nits (tar-dep assertion scope, missing nfpm binary-path assertion) are real and worth addressing in a follow-up — but A called them non-blocking and described them as "worth a follow-up," not required changes to this PR. The implementation is mechanically correct on all four regressions. Recommending approve. |
Summary
The v4.0.0
.deb/.rpmLinux packages diverged from the brew formula in three ways that brokewheels starton Rocky Linux during the paiindustries/titan production cutover on 2026-05-13. This PR aligns the Linux packaging staging pipeline with the brew formula and the Scoop manifest so all three distributions ship the same lucli-native module under the same dispatch model.Three sub-fixes + one runtime-dependency fix:
tools/distribution-drafts/linux-packages/build-linux-packages.shnow untarswheels-module-${WHEELS_VERSION}.tar.gzinto/opt/wheels/module/instead of unzipping the legacy CommandBox-shapedwheels-cli-${WHEELS_VERSION}.zip. The lucli-native artifact (~24 MB, withModule.cfcat top) is the one the brew formula and the Scoop manifest stage./opt/wheels/wheelssobasename(argv[0])iswheelswhen the wrapper execs it — mirroring the brew formula'slibexec.install resource("lucli") => "wheels". LuCLI routes via argv[0], so the rename is what makeswheels startreach the bundled wheels module.nfpm-wheels.yamlandnfpm-wheels-be.yamlnow declare/opt/wheels/.versionand/opt/wheels/.channelundercontents:. Without them,wheels --versionreturnedunknown (stable).tar, and any role that unpacks/opt/wheels/module/(or other tarball payloads) fails silently without it. Both yamls now declaretarunderoverrides.{rpm,deb}.depends.The paiindustries/titan Ansible
wheels-clirole has been carrying an overlay workaround for all three sub-bugs since 2026-05-13. With this fix, that workaround is no longer needed — and every titan VM (plus every future rpm consumer) gets a workingwheels startstraight from the package.Related Issue
Fixes #2700
Type of Change
Feature Completeness Checklist
Signed-off-by:(git commit -s)vendor/wheels/tests/specs/cli/LinuxPackageStagingSpec.cfcpin all four sub-fixes against the source ofbuild-linux-packages.sh,nfpm-wheels.yaml, andnfpm-wheels-be.yaml. Pattern followsbuildArtifactLicenseSpec.cfcandConfigRoutesStaleDocUrlSpec.cfc— structural assertions against the packaging-file source, since end-to-end execution of nfpm-built rpms requires a Rocky/Debian sandbox and signed-key access that the test runner doesn't have.bot-update-docs.ymlwill follow upbot-update-docs.ymlwill follow upbot-update-docs.ymlwill follow up[Unreleased]→### Fixedclilayer:42 pass, 0 fail, 0 error(was34 pass, 8 failbefore the implementation; the 8 failing specs are the new ones in this PR). Full core suite (Lucee 7 + SQLite):3553 pass, 0 fail, 0 error, 16 skipped— no regressions.Test Plan
vendor/wheels/tests/specs/cli/LinuxPackageStagingSpec.cfc.clilayer before and after the implementation:34 pass, 8 fail— all 8 are the new specs:build-linux-packages.sh— consumes the lucli-native wheels-module tarball, not the legacy wheels-cli zipbuild-linux-packages.sh— emits a wrapper that routes lucli through the wheels modulenfpm-wheels.yaml— ships/opt/wheels/.versionnfpm-wheels.yaml— ships/opt/wheels/.channelnfpm-wheels.yaml— declarestaras an rpm runtime dependencynfpm-wheels-be.yaml— ships/opt/wheels/.versionnfpm-wheels-be.yaml— ships/opt/wheels/.channelnfpm-wheels-be.yaml— declarestaras an rpm runtime dependency42 pass, 0 fail.http://localhost:60007/wheels/core/tests?db=sqlite&format=json) re-run:3553 passed, 0 failed, 0 errors, 16 skipped— no regressions outside the cli layer.nfpm pkgwill see in the contents block and the wrapper exec form; the actual built artifact's correctness on a Rocky 10 VM will need to be validated by re-running the paiindustries/titan cutover sequence (or a clean reproduction of it) once the rpm is published fromrelease.yml.Screenshots / Output