Repository navigation
Release 4.0.0 — merge develop into main - #2607
Merged
Merged
Conversation
…rtial) (#2277) * docs(docs): add phase 4 design spec for wheels-packages discovery UI Proposes the approach for #2271: wheels.dev/packages listing+detail pages plus a "Browse registry" section on the in-app /wheels/packages view. Two separate fetchers (wheels.dev RegistryClient; in-app reuses the CLI's Registry.cfc directly) against the same GitHub registry, 24h cache, CLI-only install path. * docs(docs): defer wheels.dev portion of #2271, add in-app plan Revises the Phase 4 spec after discovering wheels.dev is now four Astro static sites under web/sites/, not a CFML app. The in-app browse-registry section ships first; the wheels.dev Astro work needs its own brainstorm against the new architecture (build-time fetch + repository_dispatch). Adds the implementation plan for the in-app half. * feat(cli): add Registry.listAll() returning enriched package summaries * feat(view): add $loadRegistryPackages helper for in-app browse UI * refactor(view): narrow $loadRegistryPackages catch to typed registry errors * feat(view): add browse-registry section to /wheels/packages * fix(view): harden homepage/onclick rendering in browse-registry section * fix(view): use application.wheels.public (no $) in browse-registry view * docs(docs): note in-app browse-registry section for #2271 * fix(view): silently disable browse-registry when CLI classpath absent * docs(docs): clarify classpath behavior in phase 4 spec
* docs(docs): add packages.wheels.dev site design spec (#2271) Brainstorm output for the deferred wheels.dev half of #2271. Proposes a fifth Astro static site alongside blog/api/guides under web/sites/, deployed to Cloudflare Pages as wheels-packages, rebuilt via repository_dispatch fired from the wheels-packages registry repo on manifest merges. Card-grid listing, detail pages with rendered README through the unified/remark/rehype pipeline. No runtime, build-time fetch, fail-loud on registry errors. * docs(docs): add implementation plan for packages.wheels.dev site * feat(docs): scaffold packages.wheels.dev Astro site * feat(docs): add vitest to web workspace for unit tests * chore(docs): add uniform test script across all web sites * feat(docs): add registry.ts build-time fetcher for packages site * fix(docs): include URL in malformed-manifest error; document cwd gotcha * feat(docs): add markdown.ts for sanitized README rendering * test(docs): pin sanitizer behavior and empty-input handling * feat(docs): add listing page + PackageCard for packages site * fix(docs): use correct @wheels-dev/ui tokens; cache copy-button label * feat(docs): add [name].astro detail page with version history and README * fix(docs): add table caption; promote .visually-hidden to BaseLayout * feat(docs): wire packages site into web-deploy matrix with registry dispatch * test(docs): add visual-regression baselines for packages site Adds two SITES entries (packages-index + packages-wheels-sentry) to the visual-regression harness and captures their baseline PNGs. Without these baselines the visual-regression CI job fails for the new packages.wheels.dev site. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(docs): guard against manifest.name vs directory-name drift * test(docs): update packages visual baselines from CI (Linux rendering) * feat(web): add packages.wheels.dev to shared header + footer nav * test(web): refresh visual baselines after shared nav update --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* docs(docs): update packages guide for accuracy and 4.1 reality Closes #2244. The packages.mdx guide was shipped before several key pieces landed: the `wheels packages` CLI, the wheels-i18n and wheels-seo-suite conversions to 4.0 format, and before the `view` mixin-target claim was noticed as unsupported. This refresh brings the guide into sync with reality. Substantive changes to `packages.mdx`: - Remove `view` and `service` from the `provides.mixins` target list — neither is in PackageLoader's allowlist (application,dispatch,controller,mapper,model,base,sqlserver,mysql, postgresql,h2,test plus the specials global/none). Explain that `controller` already covers views since Wheels views run in the controller's variables scope. - Replace the per-method override example's `mixin="view"` with realistic `mixin="model"` and `mixin="none"` examples. - Replace "Until Wheels 4.1 lands / gh repo clone as interim" with the real `wheels packages install|list|search|show|update|remove|registry` CLI that shipped in #2276. - Update first-party package list: drop "wheels-i18n is planned for conversion" (shipped as v2.0.0) and add both wheels-i18n and wheels-seo-suite to the list of shipped packages. - Add dedicated sections: Service providers (register/boot lifecycle with ServiceProviderInterface) and Lazy loading (the `lazy: true` manifest flag for service-only packages). - Rewrite the testing section: replace the broken `directory=vendor.*` URL-param pattern (tracked by #2280) with a BDD spec example and a caution box pointing at the issue, plus guidance to rely on per-package CI. - Rewrite the publishing section with the actual registry submission checklist and flow (public repo, v<version> tag, file-type allowlist, 10 MB cap, PR title, mirror workflow bot-commits tarball+sha256). - Add a `lazy` field entry and mention `dependencies` ordering more explicitly in the manifest reference. Smaller fixes: - `glossary.mdx`: "Mixin target" entry no longer lists the non-existent `view` target. - `upgrading/3x-to-4x.mdx`: update `mixin="view"` annotation example to `mixin="model"` / `mixin="none"`. - `CLAUDE.md`: add a cross-link to the public packages guide and to wheels-packages/CONTRIBUTING.md, and refresh the first-party package list to include wheels-i18n and wheels-seo-suite. No code changes — docs and instructions only. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * test(docs): refresh packages-index visual baseline The /packages landing page now renders wheels-i18n and wheels-seo-suite tiles alongside the original four first-party packages (shipped earlier today via wheels-dev/wheels-packages#3 and #4, closes #2268 and #2267). The existing baseline predates those registry entries, so visual-regression reports 34,529 pixels differ. Diff reviewed — only the two new package tiles; layout, typography, header, and existing tiles unchanged. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…2282) * ci(config): auto-refresh packages-index baseline on registry updates Every time a new package is added to wheels-dev/wheels-packages the registry fires a repository_dispatch at this repo, which rebuilds packages.wheels.dev via the existing Deploy packages job. But the committed visual-regression baseline at web/tests/visual-baselines/packages-index.png was captured BEFORE the new package tile, so the next unrelated PR hits a spurious visual- regression failure (happened on #2281 today, following #2267 and #2268). Add a `refresh-packages-baseline` job that runs after `deploy` on the same registry-updated dispatch. It builds the packages site, regenerates just the packages-index baseline via the existing visual-regression script's --site flag, and if the PNG changed, opens a chore(docs) PR with the refreshed baseline. Security: the job intentionally does not interpolate the github.event.client_payload into any shell-expanded string. Commit messages and PR bodies are built via printf into temp files and passed via --body-file / -F, so no heredoc/indentation traps and no injection surface if the dispatcher ever comes from somewhere other than the trusted wheels-packages repo. CI gotcha: PRs opened by GITHUB_TOKEN don't trigger other workflow runs (GitHub's infinite-loop guard). Reviewers should eyeball the diff before merging; switch to a PAT if auto-CI on the bot PR becomes necessary. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * ci(config): move refresh-packages-baseline to its own workflow file Having the job inside web-deploy.yml with an `if:` guard on repository_dispatch caused it to show up as a "skipping" check on every regular PR (noise in the checks list, potentially confusing for branch-protection rules). Moving it to its own workflow file — which only subscribes to `repository_dispatch: registry-updated` and `workflow_dispatch` (manual) — means the job never shows up in PR CI. Behavior is unchanged; same job, same security posture (env-var guards, printf-into-tempfiles for commit/PR text). Also added `workflow_dispatch:` so you can trigger a one-off refresh from the Actions tab if needed. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Closes #2249. Issue 2249 asked whether to keep or remove `"lazy": true` support in package.json. Decision: keep. The feature is small (~15 lines in PackageLoader), legitimately useful for third-party service-only packages, and zero audit debt — no first-party package currently qualifies, so we're not carrying adoption weight for an unused feature. ## What's already in place - **Guide:** `web/sites/guides/src/content/docs/v4-0-0-snapshot/digging-deeper/packages.mdx` has a dedicated `## Lazy loading` section and the `lazy` field is documented in the manifest reference (added in #2281). - **Tests:** `vendor/wheels/tests/specs/packages/PackageLoaderSpec.cfc` already covers the happy path: eager-skip, `isPackageLoaded`, and on-demand instantiation via `getPackage()`. ## What this PR adds - **CHANGELOG entry** under Unreleased / Changed documenting the retain-and-document decision, first-party audit result (no candidates), and pointer to the guide. - **Defensive spec** for the `canBeLazy` gate: a package declaring `lazy: true` alongside `mixins: "controller"` must still be loaded eagerly — if the lazy flag were honored the mixin tables would be incomplete at boot. New fixture at `tests/_assets/packages/lazyignored/`. ## Audit of first-party packages All six first-party packages declare `mixins: "controller"` and therefore cannot be lazy-loaded (lazy requires mixins=none and no middleware). No conversion work to do: | Package | mixins | Candidate? | |---|---|---| | wheels-sentry | controller | no | | wheels-hotwire | controller | no | | wheels-basecoat | controller | no | | wheels-legacy-adapter | controller | no | | wheels-i18n | controller | no | | wheels-seo-suite | controller | no | The feature remains valid and tested for any future third-party service-only package that benefits. Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
) Extends the #2256 allowlist check to per-method mixin metadata overrides in $collectMixins. A typo like `function foo() mixin="controler" {}` is no longer silent — the package is recorded in failedPackages with an error naming the package, method, unknown target, and the allowlist. $validateMixinTargets gains an optional methodName context param; the per-method call happens in a validation pre-pass so a typo on method N cannot leave methods 1..N-1 half-registered in variables.mixins. Closes #2257.
…extends (#2285) The static `checkMixinCollisions` scan in Doctor.cfc missed three real blind spots against the runtime `PackageLoader.$collectMixins` semantics: per-method `mixin="..."` attributes (including `mixin="none"`), methods inherited via in-package `extends`, and function-like text inside block-comment docblocks producing false positives. The scanner still stays text-based (no `CreateObject().init()` — packages can have wheels-scope dependencies that fail pre-boot). The upgrade is semantic: strip `/* ... */` comments, walk balanced parens to capture per-method attributes in the signature tail, and follow `extends="X"` within the package dir with cycle and depth guards. Last-write-wins in the method map matches runtime inheritance where the child's redeclared method shadows the parent's. Residual limitations documented in the function docblock: out-of-package `extends` and runtime-composed methods are still invisible statically, but the authoritative runtime detection already catches both. Closes #2260
The wheels-packages registry updated and the rebuilt packages.wheels.dev index now differs from the committed visual-regression baseline. This is the expected change — new package tiles on the listing page. Refreshed automatically by .github/workflows/refresh-packages-baseline.yml on registry-updated dispatch 104fcb3. Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
…#2287) * feat(cli): add --verbose gating to wheels doctor mixin collisions Default output now shows a single-line count summary; full per-collision detail is enumerated only under `wheels doctor --verbose`. Structured collision entries live on `results.mixinCollisions` so the service stays presentation-neutral. Closes #2261 * fix(cli): gate packages extraction on tar exit code, not stderr `$extract()` threw on any non-empty stderr, which tripped on GNU tar's informational warnings ("Ignoring unknown extended header keyword 'LIBARCHIVE.xattr.com.apple.provenance'") when extracting macOS-authored tarballs on Linux. Extraction still succeeded — exit code 0 — so the stderr check was spurious. Switch to cfexecute's `result=` arg and throw only when `exitCode != 0`, preserving stderr in extendedInfo for diagnostics. Fixes 4 CI errors in InstallerSpec / PackagesMainCliSpec that were previously masked by TestBox's negative totalError accounting.
…#2288) The GitBook source tree under docs/src/ was deleted in PR #2189 when the wheels.dev publishing pipeline was retired, but the in-app /wheels/guides endpoint still read from it — every fileExists() returned false, so the page silently rendered the "Select a guide from the navigation menu" placeholder fallback. Rewrite guides.cfm to redirect HTML callers to guides.wheels.dev with a meta-refresh + visible section list, and return a sidebar-derived JSON summary to AI/MCP callers. ai.cfm::getGuidesSummary() now flattens the Starlight sidebar into 79 guide entries with external URLs. Delete the GitBook-era renderer (layouts/guides.cfm, layouts/guides-plain.cfm, DocsHelper.cfc) — ~700 lines no longer used. Runtime-verified via LuCLI: HTML landing renders, JSON returns the 10 top-level sections + deep-link URL for ?path=, /wheels/ai lists 79 flattened guides. Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
`$parseTarget` called `Left(target, Find("@", target) - 1)` without
guarding against targets starting with `@` (e.g., `wheels packages
install @1.0.0`). That evaluates to `Left(str, 0)`, which crashes on
Lucee 7 per the documented gotcha in CLAUDE.md and produces a cryptic
empty-name install attempt on other engines.
Throw `Wheels.Packages.BadInput` explicitly when the name segment is
empty, with a message that points at the correct syntax. Preserves the
existing valid-input paths (no `@`, or `name@version`).
Adds a regression spec in PackagesMainCliSpec.
…ut (closes #2279) (#2289) wheels new copied vendor/wheels/box.json verbatim from the resolved framework source. When that source was a dev checkout of the wheels-dev/wheels monorepo, the unreplaced @build.version@ placeholder propagated into the new app — the release pipeline substitutes it at build time, but raw checkouts do not. The framework's $readFrameworkVersion() fallback only identifies the monorepo when the enclosing box.json is the monorepo's own, never true inside a generated app, so the homepage reported "0.0.0-dev" forever. copyFrameworkToVendor now delegates to a new FrameworkInstaller service that post-processes the copied box.json: if it still contains @build.version@ AND the source's enclosing root box.json identifies the monorepo (slug=wheels or name=Wheels.fw) AND has a real version, rewrite the copy's placeholder to "<rootversion>-dev". Released bundles (real version already substituted) and third-party layouts (no monorepo markers) early-return, so no new failure modes. Isolated the rewrite into a service so specs can exercise it without instantiating Module.cfc (which requires a modules.BaseModule mapping only present at LuCLI runtime).
) The core test runner at /wheels/core/tests hardcoded directory="wheels.tests.specs" when instantiating TestBox, so the documented `&directory=...` URL param was silently dropped. Package specs in vendor/<name>/tests had no HTTP entry point even though docs advertised the pattern. Read url.directory and pass it through, guarded by a conservative allowlist (wheels.tests.* or vendor.<pkg>.tests.*) since the endpoint is unauthenticated — defense-in-depth against arbitrary CFC compilation through whatever mappings happen to be registered. Verified end-to-end against a fresh LuCLI server: - baseline full suite: 3327 pass / 182 bundles / 24.2s - &directory=wheels.tests.specs.model: 803 pass / 32 bundles / 1.6s - &directory=vendor.wheels-fake-pkg.tests: 1 pass / 1 bundle / 6ms - &directory=../../../etc (rejected): 3327 pass (falls back to default) Also flips the "not honoured yet" aside in packages.mdx to working guidance, and adds the per-package row to the filter table in running-tests-locally.mdx. Closes #2280 Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…rkVersion (closes #2291) (#2293) The default rootBoxJsonPath resolved "../box.json" from Global.cfc's directory (vendor/wheels/), pointing at vendor/box.json which does not exist. At runtime boot via onapplicationstart.cfc — where the helper is called with no arguments — this silently fell through to "0.0.0-dev" even inside a wheels-dev/wheels checkout. Walking up two levels reaches <repo-root>/box.json so application.wheels.version now resolves to "<rootversion>-dev" as intended. Adds a regression spec that asserts application.wheels.version at boot (rather than invoking the helper directly), because WheelsTest.cfc binds methods into each spec's scope which shifts GetCurrentTemplatePath() and bypasses the code path we need to cover. The existing specs all passed explicit path arguments, leaving the default-args computation uncovered. Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
#2294) * fix(cli): improve framework-source discovery errors and doctor detection - `wheels new` error now links to the real installing page instead of /docs/getting-started (404), and mentions Homebrew/Chocolatey packaging explicitly so users distinguish "wrong directory" from "incomplete install." - `wheels doctor` detects missing vendor/wheels/ via new checkFrameworkSourceBundled(), issuing CRITICAL when the CLI can't resolve framework source the same way Module.cfc.resolveFrameworkSource() does. - Recommendation engine now offers "reinstall or set WHEELS_FRAMEWORK_PATH" instead of misleading "Run 'wheels new'" when framework source is absent. Catches the onboarding dead-end where a fresh Homebrew install scaffolds an app, can't boot it, and doctor points at the same broken command. * fix(config): publish full artifact set on snapshot releases Snapshot pre-releases from `develop` now attach wheels-core-*.zip, wheels-base-template-*.zip, wheels-cli-*.zip, and wheels-starter-app-*.zip alongside wheels-module-*, mirroring what main-branch releases publish. Downstream packagers (Homebrew, Chocolatey) depend on wheels-core-*.zip to bundle framework source alongside the CLI module. Until now, no snapshot had ever published that asset, so every snapshot-based package install dead-ended at "wheels new" with a framework-not-found error. Adopts the CommandBox scheme: develop = snapshot prerelease, main = final, identical artifact set differentiated only by the prerelease flag.
…2298) Two help-text lines added in #2294 contained a single literal `#` (shell-comment marker shown in the manual-recovery instructions). CFML treats `#` as the start of a `#expr#` interpolation, so Lucee parsed `# Pick the wheels-core-<version>.zip…` as an unterminated interpolation and failed Module.cfc compilation: Invalid Syntax Closing [#] not found, at [3540:22] This broke the post-merge Wheels Snapshots run on develop — the "Smoke Test Installed Distribution" job in the same release workflow the original PR added to catch exactly this class of regression. Doubled `#` to `##` on both lines (CFML's literal-hash escape). The other `#` references in the surrounding block (`#appName#`, `#candidate#`, `#projectCandidate#`) are proper interpolations and remain unchanged. Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
) (#2300) Add a line to the Aside on the Installing page explaining that `brew install openjdk@21` is keg-only and Homebrew won't symlink it into `/opt/homebrew`. The `wheels` CLI handles this internally, but calling `java` directly (or from IDEs, Maven, Gradle) requires `JAVA_HOME` and `PATH` to be set. Also tighten the Troubleshooting block to use `brew --prefix openjdk@21` instead of a hardcoded versioned Cellar path (rots on point releases) and add the missing `PATH` export. Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…-here (closes #2296) (#2301) The Start Here → Installing Windows tab told users to run `choco install wheels`, which still resolves to the v1.x CommandBox-based legacy package on the Chocolatey community feed. Following the tutorial verbatim left Windows users with a binary that cannot drive the v4 walkthrough at all. Replace the bare `choco install` with a caution Aside and inline manual JAR steps mirroring the deeper CLI Installation reference. Update the Upgrading Windows tab to match. Restore `choco install wheels` once the v2 LuCLI-based package ships from `wheels-dev/chocolatey-wheels`. Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
#2297) Bumps [postcss](https://github.com/postcss/postcss) from 8.5.3 to 8.5.10. - [Release notes](https://github.com/postcss/postcss/releases) - [Changelog](https://github.com/postcss/postcss/blob/main/CHANGELOG.md) - [Commits](postcss/postcss@8.5.3...8.5.10) --- updated-dependencies: - dependency-name: postcss dependency-version: 8.5.10 dependency-type: indirect ... Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
* fix(docs): restore search bar in api/guides starlight header (closes #2299) Custom Header override replaced Starlight's default header without re-mounting <Search />, so api.wheels.dev and guides.wheels.dev lost the search/filter field. Add a conditional `search` slot to the shared Header and fill it from the Starlight override using `virtual:starlight/components/Search`, matching the pattern already used in Footer.astro for EditLink / LastUpdated / Pagination. Landing/blog/packages sites are unaffected — they don't pass a search slot, so the container is not rendered (Astro.slots.has guard). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * test(docs): refresh guides visual baseline for restored search button Header now renders Pagefind's <Search /> button per #2299 fix. Diff confirms only the new search button area changed (202px > 200 threshold). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
`wheels new` was emitting `bundleName: "org.xerial.sqlite-jdbc"` into the generated config/app.cfm, which routes Lucee 7's datasource resolver through BundleProvider. That code path currently fails on all platforms because the upstream S3 listing at bundle-download.s3.amazonaws.com contains a malformed entry (`javassist-3.9.0.GA copy.jar`) whose space + " copy" suffix is both an illegal URI character and an invalid OSGi version qualifier. The listing parse throws, bundle resolution dies, and the SQLite driver never loads — leaving fresh `wheels new` apps with a 0-byte database and a `wheels migrate latest` that silently no-ops. Removing the bundleName hint lets Lucee fall through to the standard classpath, which resolves org.sqlite.JDBC normally as long as sqlite-jdbc is on the lib path. Companion changes to bundle the JAR in homebrew-wheels and chocolatey-wheels will follow. Reported by a fresh-VM tutorial onboarding run on macOS arm64. Note: cli/src/ (legacy CommandBox surface) still emits the same bundleName hint and will need a matching change if that distribution is still being shipped.
…2306) * fix(docs): correct tutorial gaps surfaced by fresh-vm onboarding run Walk-through on a brand-new macOS VM hit a string of doc bugs that broke the tutorial at multiple chapters. This patch rolls all copy-edit fixes (no engineering required) into one pass. Tutorial chapters: - chapter 02 (seeds) — wrap `seedOnce(...)` block in `<cfscript>...</cfscript>`, add a note explaining why. Without the wrapper, `wheels seed` reports `0 created, 0 skipped` because Lucee parses .cfm as markup by default. - chapter 03/04 (form helper) — `dateTimeField` does not exist in Wheels 4.0; substitute `dateField`. Already documented as missing in basics/forms-and-form-helpers.mdx. - chapter 05 (cross-controller partial) — `partial="comments/form"` resolves into the current controller's view folder; the leading slash on `/comments/form` is what triggers absolute-path resolution. Also build an unsaved Comment via `model("Comment").new()` before the include so the partial's `errorMessagesFor("comment")` helper has a real model object to inspect on first render. - chapter 06 (auth callback timing) — register `hashPassword` as `beforeValidation`, not `beforeSave`. Wheels' automatic `validatesPresenceOf` rules treat NOT NULL `passwordHash` and `passwordSalt` columns as required; with `beforeSave` the columns are still empty when validation fires, so every signup fails with `Passwordhash can't be empty`. - chapter 07 (test command) — `wheels test run` falls through to LuCLI's `run SCRIPT` parser; the wired-up subcommand is `wheels test`. Cross-doc cleanup of the same root-cause bugs: - `wheels test run` -> `wheels test` across testing/ pages, the glossary, and the tutorial index. Same broken command, would-be inconsistent with chapter 07 if left. - `beforeSave("hashPassword")` -> `beforeValidation("hashPassword")` in digging-deeper/authentication-patterns.mdx, same root cause as chapter 06. Install page: - Replace the example `wheels --version` output line; the format shipped by 0.3.7 does not match `Wheels Framework v4.0.0 (LuCLI 0.3.5-SNAPSHOT)`. Now describes the actual two-line format without pinning a specific version string. Out of scope (filed as separate issues): - chapter 06b's `application.wo.service(...)` API references — needs canonical DI surface decision before the doc fix lands. - The CLI bugs themselves (silent migrate failures, scaffold abort on existing files, `wheels test` core/app mode detection, broken `wheels browser install`, etc.) — separate PRs. * fix(docs): correct ch01/ch02 'What got created' trees against real wheels new output The "What got created" tree in chapter 01 and the "Where we left off" recap in chapter 02 were missing several directories that `wheels new` actually generates. A fresh-VM tutorial walker called the trees out as "at minimum stale and at worst misleading" because users `ls` their new app and see directories the docs don't mention (events, global, jobs, lib, mailers, plugins, snippets) plus root-level files (.env, .gitignore, lucee.json) — and config/app.cfm, db/test.sqlite, public/* contents, tests/specs/* — that the trees omit. Captured the actual `wheels new` output via the local onboarding harness and updated both trees to match. Chapter 01 now shows the full tree (this is the chapter where the reader first looks at the layout) with a paragraph explaining what the additional app/ subdirs are for. Chapter 02 keeps a focused tree (only what that chapter touches) but the "Where we left off" prose now explicitly notes that the rest of the layout is elided for focus, so the reader knows the truncation is deliberate rather than a stale doc. Tree captured from `wheels new clean-app` (LuCLI 0.3.7, wheels module 4.0.0-SNAPSHOT) on macOS arm64. Re-running with later wheels-cli versions may surface drift; the onboarding harness in PR #2308 is the right place to wire up automated tree-drift detection if we want guards. Reported as F4 in the second VM-onboarding journal.
#2307) Both Module.cfc and MigrationRunner.cfc were mapping the user-visible `latest` action to the framework HTTP command name `migrateTo`. The framework's `/wheels/cli` endpoint treats `migrateTo` as "migrate to a specific version" — and silently no-ops when no version parameter is provided. The correct command name is `migrateToLatest`, which the endpoint dispatches via `migrator.migrateToLatest()`. Without this fix, `wheels migrate latest` exits 0 with the message "Migration latest completed." in green, but no migration is actually applied. The first VM-test journal observed this as "Migrating from 0 down to ." (the framework rolling back to nothing); the second VM-test journal observed it as a 0-byte database file with no schema. Both green CLI output, both broken framework behavior. A `&version=` empty appendix in Module.cfc (with a misleading comment claiming "omitting it runs to latest") was the smoking gun — that comment described the author's intent, not the framework's actual behavior. Discovered via local fresh-install harness (tools/test-onboarding.sh) which simulates a brand-new user running `wheels new` + `wheels migrate latest` in an isolated LUCLI_HOME and verifies the resulting sqlite database actually contains the migrated tables. Reported as F2 / F5 in two consecutive VM-onboarding runs.
…2308) Local harness that simulates the brand-new-user onboarding flow without touching the user's daily wheels install. Uses LUCLI_HOME isolation so an isolated copy of the worktree's cli/lucli/ is exercised, with the worktree's vendor/wheels/ as WHEELS_FRAMEWORK_PATH. Phases (mirrors fresh-VM journal format for direct comparison): - Phase 1: Setup isolated LUCLI_HOME (worktree mount, framework path, Lucee Express symlink to avoid the 74MB re-download) - Phase 2: wheels new (assertions for F3 dup create lines, F4 file tree shape, F1 no bundleName in generated config/app.cfm) - Phase 3: server boot via 'lucli server run' + sqlite-jdbc shim - Phase 4: migration cliff (F2/F5 false-success — verifies the actual sqlite db has the migrated tables, not just exit 0) - Phase 5: seed (cfscript wrapper + idempotency) - Phase 6: CRUD walkthrough through chapters 2-3 - Phase 7: wheels packages list (F7 — currently SKIP pending a separate fix; reports the bug signature when it reproduces) Closes the iteration loop: instead of fix → push → fresh-VM test (30+ min/round), we can now fix → run harness (~90s) and only burn a fresh VM as the final confidence check. Modes: - bash tools/test-onboarding.sh # symlink mount (default) - MODE=copy bash tools/test-onboarding.sh # copy (closer to brew install) - BASELINE=1 bash tools/test-onboarding.sh # use brew-installed wheels - KEEP_TEMP=1 bash tools/test-onboarding.sh # preserve temp dirs - FROM_PHASE=4 bash tools/test-onboarding.sh # skip earlier phases JAVA_HOME is auto-resolved (via /usr/libexec/java_home or brew-prefix fallback for keg-only openjdk@21). LUCLI_HOME ends in '.lucli' so LuCLI's getComponentPath() picks the absolute-file-path mode for Module.cfc loading. Without this harness we'd have shipped PR #2304 + #2306 + the formula updates, watched the next VM run still fail at migrate (the migrateToLatest bug surfaced in PR #2307 was masked by the silent exit-0), and needed a third VM iteration to find it.
…mapping (#2309) * fix(cli): resolve cli.lucli.* component paths via the modules.wheels mapping Replaces every `cli.lucli.services.*` absolute dotted path in cli/lucli/ with the equivalent `modules.wheels.services.*` path. The new prefix matches the same `modules.<name>.<...>` convention LuCLI uses to load each module's own `Module.cfc`, so the paths resolve identically in every context the CLI runs in: Dev symlink (~/.lucli/modules/wheels -> <repo>/cli/lucli) modules.wheels.services.X -> <repo>/cli/lucli/services/X.cfc Brew install (~/.wheels/modules/wheels) modules.wheels.services.X -> ~/.wheels/modules/wheels/services/X.cfc Isolated harness ($LUCLI_HOME/modules/wheels) modules.wheels.services.X -> $LUCLI_HOME/modules/wheels/services/X.cfc The previous `cli.lucli.*` prefix only resolved when the module was loaded from a path with `cli/lucli/` in its filesystem ancestry — true for the user's daily dev symlink (because the symlink target lives in the repo's `cli/lucli/` directory and Lucee follows the symlink to its real path), but NOT true for any installation that copies the module's contents (Homebrew bottle, Chocolatey package, or any LUCLI_HOME-isolated test setup). User-visible effect on fresh installs: Before: $ wheels deploy version Error: could not find component or class with name [cli.lucli.services.deploy.cli.DeployMainCli] $ wheels packages list Error: could not find component or class with name [cli.lucli.services.packages.PackagesMainCli] After: $ wheels deploy version wheels-deploy mirrors kamal 2.4.0 / kamal-proxy v0.8.6 $ wheels packages list [reaches the next level — see follow-up note below] Reported as F7 in the second VM-onboarding journal. Validated locally via the onboarding harness (PR #2308): with this fix applied, the `wheels deploy` family resolves cleanly in the LUCLI_HOME-isolated test that simulates a fresh brew install. Scope: 88 files in cli/lucli/, 482 -> 486 prefix substitutions (small discrepancy because a handful of files had >1 ref per line via comments or chained calls). Includes test specs so they continue to mirror production paths. Out of scope (separate follow-up): `wheels packages list` now reaches PackagesMainCli but fails at cli/lucli/services/packages/VersionResolver.cfc:17 which does `new wheels.SemVer()` — a reference to the framework's `vendor/wheels/SemVer.cfc`. The framework isn't on the CLI's resolution path when running outside an app, so SemVer can't be found. That's a different architectural issue (CLI command depends on framework being loaded) and warrants its own PR. The harness Phase 7 will continue to surface it as a SKIP until that's fixed. * fix(cli): add /modules/wheels mapping in test Application.cfc Test specs under cli/lucli/tests/specs/ now reference services via the same `modules.wheels.X` dotted path the production code uses (post the cli.lucli -> modules.wheels rewrite earlier in this PR). The test Application.cfc already had /cli, /wheels, and /vendor mappings for the old `cli.lucli.X` paths, but no /modules/wheels mapping for the new prefix — so the spec runner couldn't resolve references like modules.wheels.services.deploy.commands.Base. Add the missing mapping so /modules/wheels resolves to cli/lucli/ the same way LuCLI's runtime maps modules.<name> at module-load time. * fix(cli): keep test specs on cli.lucli.* prefix Reverts the cli.lucli -> modules.wheels rewrite for files under cli/lucli/tests/. The earlier global sed touched test specs along with production code, breaking the test runner because the test Application.cfc's mappings only register /cli, /wheels, and /vendor — not /modules/wheels in a way that resolves multi-segment virtual paths the same as LuCLI's runtime does. Reverting the test specs to cli.lucli.* keeps them resolvable via the existing /cli mapping (well-tested, has been in place since the deploy subsystem landed). Production code keeps the new modules.wheels.* prefix so it resolves in install contexts where /cli/lucli/ is not in the filesystem hierarchy. The /modules/wheels mapping in cli/lucli/tests/Application.cfc stays — it's still needed for service-internal cross-references that fire when a test instantiates a production component. This pairs the two prefix conventions cleanly: - test specs: cli.lucli.X (resolves via /cli mapping in tests) - prod code: modules.wheels.X (resolves via LuCLI runtime + tests mirror that with /modules/wheels mapping) * fix(cli): register /modules/wheels mapping at framework webroot for CI tests The cli.lucli -> modules.wheels prefix rewrite earlier in this PR broke the framework's own CLI test path (/wheels/cli/tests via public/Application.cfc) because the production services use modules.wheels.X for their internal cross-references but the framework webroot's Application.cfc had no /modules/wheels mapping registered. The error in CI: "invalid component definition, can't find component [modules.wheels.services.deploy.secrets.BaseAdapter]" — fired when a test loaded a DopplerAdapter (etc) that extends BaseAdapter via the modules.wheels.* prefix. Adding the mapping at public/Application.cfc and matching it in cli/lucli/tests/Application.cfc keeps both code paths happy: - /wheels/cli/tests (framework webroot Application.cfc) — gets it now - /cli/lucli/tests/runner.cfm (CLI test runner) — already had it User apps don't need this mapping — they don't load CLI internals.
Adds a "Local Onboarding Harness" section to CLAUDE.md (auto-loaded into every Claude Code session in this repo, so future sessions know the harness exists and when to reach for it) and a deeper reference file at .ai/wheels/testing/onboarding-harness.md (auto-searched when a session is specifically about testing). CLAUDE.md gets a Quick Reference style block listing the modes, phases, and finding-ID mapping. The deeper .ai/ doc covers how it works internally, when to reach for it vs the other test scripts, required state, and the design decision behind keeping it separate from tools/test-local.sh. The harness shipped in PR #2308 but only the script header was discoverable. This makes it findable from the top-level project documentation that Claude reads at session start.
Fires `repository_dispatch` events of type `wheels-released` to wheels-dev/homebrew-wheels and wheels-dev/chocolatey-wheels after the GitHub release (snapshot or final) is published. Their auto-update workflows already accept the trigger; this drops the "wheels release -> package manager bump" latency from up to 24h (cron) to ~5min. Requires a DOWNSTREAM_DISPATCH_TOKEN secret (fine-grained PAT or app token with `metadata: read` + `actions: write` on the two downstream repos). When the secret is missing, the step warns and exits 0 so the release itself is never blocked — cron picks up. Static event type and a fixed repo list, no untrusted input flows into the curl arguments. Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…ding (#2321) Three small fixes against the v4-0-0-snapshot start-here tutorial that the 2026-04-25 fresh-VM onboarding run flagged: * Tutorial 02 — replace the "0.3.5-SNAPSHOT generator can't run" caveat with the real generator flow. `wheels generate model` works on current 0.3.7+ (the snippet-template gap was closed in #1944). The generator output is slim, so we walk the user through augmenting it with the `enum` and the extra columns instead of "hand-write three files." * Tutorial 02 — `wheels new` does not scaffold `app/db/`, so the seed-file step now starts with `mkdir -p app/db`. (A separate CLI follow-up will make `wheels new` produce the directory.) * Tutorial 03 — fix `wheels destroy controller Posts` to the actual CLI argument order (`<name> [type]`). Replace the "scaffold needs templates that don't ship" preamble with the real failure mode the user now hits: scaffold-on-existing-model bails with "Model already exists". Hand-write fallback stays as the recovery. Refs the open CLI issues for the underlying ergonomics gaps: - destroy arg-order accept-both: future CLI follow-up - scaffold --skip-existing: future CLI follow-up Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds three new phases to tools/test-onboarding.sh that detect regressions of issues surfaced by the fresh-VM onboarding run on 2026-04-25: * Phase 8 — wheels routes returns route table not API JSON dump (#2317) * Phase 9 — wheels test prints non-zero counts when a spec exists (#2318) * Phase 10 — dev error pages return 5xx/4xx not HTTP 200 (#2319) Each new phase SKIPs cleanly while its open issue stands and flips to PASS once fixed — the harness stays green during normal development and lights up regressions automatically. Phase 7 (wheels packages list) gains a second discrimination branch: the original PackagesMainCli failure was fixed in #2309, but a follow-on `wheels.SemVer` dotted-path issue surfaces only after that fix landed. The harness now distinguishes "the original bug regressed" from "the follow-on bug is reproducing" so the output is actionable. The follow-on is filed as a separate issue. Phase 10 caveat: hits /__definitely_not_a_route which returns 404 correctly — the original Wheels.DataSourceNotFound case from the fresh-VM run can't be reproduced in the harness because the sqlite shim makes the datasource work. The 404 path is still useful as a regression check on the dev error page status code. Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…ml (#2324) The release.yml workflow runs in two modes: 1. Push to main — secrets are inherited from the workflow run 2. workflow_call from snapshot.yml — secrets must be explicitly declared on the callee and explicitly passed through on the caller #2320 added the dispatch step + read of secrets.DOWNSTREAM_DISPATCH_TOKEN but only handled mode 1. In mode 2 (which is what snapshot.yml does on every develop push) the secret is invisible regardless of whether the repo has it set, so the dispatch step always hits its graceful-degrade warning branch. Fix is a 7-line passthrough: declare the secret as optional in the workflow_call.secrets block of release.yml, then pass it through from snapshot.yml. Since the step exits 0 when the secret is missing, no release ever breaks if the secret is unset — this just makes the secret actually visible when it IS set. Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* ci: relax visual regression + commit scope checks Visual regression now skips when all changed files are content-only (markdown anywhere under web/, Astro content collections under web/sites/*/src/content/, free-standing content under web/content/, and static assets under web/sites/*/public/). A new detect-scope job emits the decision; visual-regression gates on it via needs+if. Mixed PRs still run the full regression with the existing blog-baseline pre-refresh path. Drops the commitlint scope-enum allowlist. Scope is still optional, but any value is now accepted — the allowlist produced more friction (wrong scope -> CI block -> amend + force-push) than value (changelog grouping keys off type, not scope). Conventional types and the subject/header rules remain enforced. Updates CLAUDE.md "Commit Message Conventions" and the wheels-bot shared rails so humans and bots get the same loosened guidance. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * ci: upload visual-regression diffs before failing the job Previous ordering ran 'Fail job on real regression' first (which exits 1), which caused Actions to skip the subsequent 'Upload diffs on failure' step even though its 'if:' matched. Net effect: the diagnostic artifact went missing on exactly the runs that needed it. Reorder so the upload runs first, then the explicit-fail step asserts the failure for the check status. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * chore(web): refresh blog visual baseline after LuCLI post The previous blog-post recovery (66827a2) on develop didn't trigger the post-merge auto-refresh, so every subsequent PR was tripping on the stale baseline. Pulling the actual screenshot from this PR's own CI run and committing it as the new baseline clears the trap. This is exactly the failure mode the content-only skip in this PR prevents going forward — once landed, content-only PRs skip the gate entirely. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…er failed auto-deploy) (#2580) * docs(web/blog): LuCLI and the Zero-Docker Developer Experience (recover failed auto-deploy) The scheduled auto-deploy for this post failed at 2026-05-11 08:19:00 with a rebase conflict on an unrelated file (multi-tenancy-built-in.md). Root cause: GitDeployer rebases the current branch onto origin/develop without ensuring the working tree is on develop first; when the wheels repo was left on a feature branch (peter/bot-convergence-loop) that had already-merged commits, the rebase replayed them and hit add/add. The export ran successfully (lastExportedAt=2026-05-11 08:19:00, body matches DB), so this is purely the missing commit. Same recovery pattern as #2551 (multi-tenancy). Companion fix landed in wheels-publishing-admin (PR #2) hardens GitDeployer to ensure develop checkout before deploy, so tomorrow's launch article does not hit the same trap. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * docs(web/blog): correct CLI surfaces in LuCLI post Two factual CLI errors flagged by wheels-bot review: - `wheels dbmigrate latest|up|down|info|diff` conflated two distinct surfaces. The LuCLI module exposes `wheels migrate latest|up|down|info`; `wheels dbmigrate diff` is the separate auto-migration surface. Split into two bullets so readers on a pure-LuCLI install run the right command. - `wheels server start|stop|status` used a non-existent `server` prefix. Server lifecycle is top-level: `wheels start|stop|status` (matches CLAUDE.md Development Tools table and the post's own prose four paragraphs earlier). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Two real-world failures surfaced when PR #2583 (a content-only blog post polish) became the first PR to exercise the content-only skip: 1. detect-scope died with exit 1 on a content-only diff. The script used 'set -uo pipefail' but GitHub Actions runs bash with -e by default, and 'set -uo' doesn't touch -e. When every changed file matched the content patterns, the final 'grep -vE' produced no output and exited 1 (normal grep behavior), set -e escalated that to a job failure. Fix: explicit 'set +e' + a '|| true' guard on the offending grep chain. 2. Because visual-regression has 'needs: detect-scope', Actions auto-skipped visual-regression when detect-scope failed — a silent bypass of the layout gate, the exact failure mode the safety-default comments in the job warned against. Fix: change visual-regression's if: to 'always() && (detect-scope failed OR content_only != true)' so a broken detector ALWAYS runs the regression rather than silently skipping it. Together: detect-scope can no longer die on its intended use case, and even if it did, the regression would still run as a fail-safe. Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…eline (#2585) Two follow-on fixes after the detect-scope grep bug exposed a deeper design issue: 1. The content-only skip in #2581 applied to both PR and push events. For PR events, that's the friction-reducer we wanted. For push events to develop, it silently broke the blog baseline auto-refresh-and-commit dance, which lives inside the visual-regression job. After #2580 merged, the post-merge push couldn't refresh the baseline (detect-scope had crashed; even if it hadn't, content_only=true would have skipped the whole job), so every subsequent non-content PR trips on the stale baseline — exactly what's happening now on this hotfix PR. Fix: tighten the visual-regression `if:` to skip only on PR events with content_only=true. Push events always run, eating the few extra CI minutes so the maintenance contract holds. 2. Refresh blog.png from this PR's CI artifact. The previous baseline was screenshot before #2580 merged and is now 38k pixels off the current develop blog index. Pulling the actual.png from the visual-regression-diffs artifact and committing it here. Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Pre-existing nits surfaced by wheels-bot Reviewer A on the now-merged LuCLI post: - Engine support: the original sentence listed Lucee 5, Adobe 2018, and Adobe 2021 alongside the CI-gated engines, but CLAUDE.md explicitly notes those three are NOT in the matrix and should be considered unsupported for new development. Reframe so the CI matrix is what's "supported" and the legacy versions are called out as not CI-gated. - "Wheels CLI installers" anchor pointed only to homebrew-wheels — misleading for Windows readers. Drop the outer anchor so the inline Homebrew + scoop-wheels links carry platform-specific navigation. Both nits were flagged as non-blocking on PR #2580 and merged through; addressing them now as a follow-up so the published post is accurate. Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The `anthropics/claude-code-action@v1` action defaults to rejecting actors without write permission to the repo — a sensible default for PR-related automation, but exactly backward for issue triage on a public OSS bug tracker, where contributors filing bugs are precisely who we want responded to. Symptom: 5 of the last 13 bot-triage runs failed at the "Run Triage" step with `Actor does not have write permissions to the repository`, producing zero triage comments on issues opened by external contributors. Fix: - `allowed_non_write_users: '*'` — the action input designed for this exact case. Docs name "issue labeling" as the canonical example. - Added `workflow_dispatch` with `issue_number` input so missed issues can be re-triaged without close/reopen. - Hardened the `if:` guard so the wheels-bot self-loop check no longer evaluates `github.event.issue` on dispatch events. Safe because the workflow runs with `permissions: contents: read`, posts via an App token independent of the actor, and `claude_args` restricts the model's shell to `gh` + read-only `git` + file reads. Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* ci: switch wheels-bot triage from Sonnet to Opus Triage's job is now harder than pure classification — it reads the issue body, optionally greps the codebase, and self-rates confidence that downstream Opus stages (propose-fix, research, write-docs) auto- fire on. The auto-downgrade rules force `medium` for sensitive surfaces regardless of model, so the gate's guard-rails stay intact; upgrading the model just gives the bot a better chance of resolving genuine uncertainty (vs. hedging to `medium` when a deeper read of the code would have produced a clean `high`). Concrete example from #2582 today: triage rated `medium` because the reporter listed `/env` as working even though it routes through the same `consoleExec` function as the failing commands. An Opus pass on that issue would have either read `consoleExec` to resolve the inconsistency (likely `high`) or confirmed it as a genuine scope- initialization nuance (justified `medium` with sharper rationale). Tools and turn budget unchanged — same read-only allowlist (`gh`, read-only `git`, `Read`/`Grep`/`Glob`), same 200-turn ceiling. Cost delta is roughly 5× per triage; volume is modest (~3-4 issues/day). Also synced the model column in CLAUDE.md's bot stages table. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * docs: address Reviewer A/B consensus findings (round 1) - docs/contributing/wheels-bot.md:260 — replace stale "only stage that runs Opus on a non-coding task" claim with advisor+triage framing (A's main finding; B confirmed) - docs/contributing/wheels-bot.md:224 — clarify "analytical Sonnet stages above" by naming Reviewer A and B explicitly (B's missed-issues finding) --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> Co-authored-by: claude[bot] <41898282+claude[bot]@users.noreply.github.com>
* ci: switch wheels-bot triage from Sonnet to Opus Triage's job is now harder than pure classification — it reads the issue body, optionally greps the codebase, and self-rates confidence that downstream Opus stages (propose-fix, research, write-docs) auto- fire on. The auto-downgrade rules force `medium` for sensitive surfaces regardless of model, so the gate's guard-rails stay intact; upgrading the model just gives the bot a better chance of resolving genuine uncertainty (vs. hedging to `medium` when a deeper read of the code would have produced a clean `high`). Concrete example from #2582 today: triage rated `medium` because the reporter listed `/env` as working even though it routes through the same `consoleExec` function as the failing commands. An Opus pass on that issue would have either read `consoleExec` to resolve the inconsistency (likely `high`) or confirmed it as a genuine scope- initialization nuance (justified `medium` with sharper rationale). Tools and turn budget unchanged — same read-only allowlist (`gh`, read-only `git`, `Read`/`Grep`/`Glob`), same 200-turn ceiling. Cost delta is roughly 5× per triage; volume is modest (~3-4 issues/day). Also synced the model column in CLAUDE.md's bot stages table. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * docs(contributing): adopt DCO for contributions Wheels now requires a `Signed-off-by:` trailer on every commit, certifying the contributor's right to submit under the project's Apache 2.0 license. The [DCO GitHub App](https://github.com/apps/dco) enforces this on every new PR; existing commits are grandfathered. Changes: - CONTRIBUTING.md: new "Developer Certificate of Origin" section covering the DCO text, `git commit -s`, the amend/rebase recipes for forgotten sign-offs, enforcement, and grandfathering. Quick Links and both workflow lists updated to reference it. - .github/pull_request_template.md: new "DCO sign-off" checklist item. - .claude/commands/_shared-rails.md: wheels-bot must include a `Signed-off-by: wheels-bot[bot] <...>` trailer matching its configured git identity. - .claude/commands/{propose-fix,address-review,write-docs,update-docs}.md: example `git commit -m` → `git commit -s -m` so the bot eats its own dog food. - CHANGELOG.md: entry under "Configuration & developer experience". Closes #2575. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> Signed-off-by: Peter Amiri <peter@alurium.com> --------- Signed-off-by: Peter Amiri <peter@alurium.com> Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…UI (#2588) * fix(router): classify /_browser fixture routes as Internal in Routes UI The Routes UI predicate previously matched only `controller == "wheels.public"` or `pattern == "/wheels/app/tests"`, so the `/_browser/*` browser-test fixture routes (registered via `loadBrowserTestFixtures` in dev/test environments) fell through to the Application tab even though they ship with the framework and have no corresponding files under /app. Extend the predicate to also classify routes whose pattern begins with `/_browser` as internal. * docs(web/guides): note /_browser fixture routes appear under Internal tab in Routes UI Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> --------- Co-authored-by: claude[bot] <41898282+claude[bot]@users.noreply.github.com> Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
…atasource work (#2591) Co-authored-by: claude[bot] <41898282+claude[bot]@users.noreply.github.com> Co-authored-by: Peter Amiri <peter@alurium.com>
…#2594) cli/LICENSE was the MIT license inherited from Tom King's standalone wheels-cli CommandBox module (2016) before the monorepo merge. The root LICENSE is Apache 2.0, so the repo presented as dual-licensed to SCA tools and created legally ambiguous inbound-equals-outbound assumptions for cli/ contributors. - cli/LICENSE: replace with byte-for-byte copy of root Apache 2.0 text. - NOTICE: new root file capturing Tom King's 2016 copyright assertion per Apache 2.0 Section 4(c), satisfying the MIT notice-retention clause without preserving the conflicting license grant. No cli/box.json or cli/package.json exists declaring "license": "MIT" (verified). The only remaining MIT mentions in cli/ are (a) third-party JAR attribution for sshj transitives + slf4j in cli/lucli/lib/deploy/README.md, and (b) the user-pickable default value in the plugin scaffold command — neither is a project-level license claim. Closes #2572
… (#2593) Apache License 2.0 §4(d) requires NOTICE attribution to propagate to derivative works. PR #2594 added an initial NOTICE focused on the cli/LICENSE MIT→Apache reconciliation; this change extends the same file with the broader contributor attribution called for in #2573 — original CFWheels authors (Per Djurner et al.), the named long-running contributors, and a pointer to git log for the full list — while preserving the Tom King / wheels-cli MIT relicensing section as a dedicated subsection. Also updates tools/build/scripts/prepare-base.sh to bundle NOTICE alongside LICENSE in the base template build artifact, so the ForgeBox-published scaffold (and anything redistributing it) carries the attribution forward. Footer of NOTICE references LICENSE rather than restating the full Apache 2.0 boilerplate, matching the ASF canonical template convention (NOTICE is for attribution; license terms live in LICENSE). LICENSE itself is intentionally not modified; keeping the verbatim Apache 2.0 text intact preserves the fingerprint that license-detection tooling (FOSSA, ScanCode, GitHub linguist) relies on. Closes #2573. Signed-off-by: Peter Amiri <peter@alurium.com> Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* chore: standardize license metadata across package.json files Adds "license": "Apache-2.0" (SPDX identifier) to every package.json so SCA tooling (GitHub dependency graph, FOSSA, Snyk) classifies the project deterministically. The license field IS the SPDX declaration for JSON manifests — there is no separate SPDX-License-Identifier slot in package.json. - 9 manifests previously had no license field — now Apache-2.0. - tools/vscode-ext was MIT (looked accidental — the rest of the repo is Apache-2.0 and Microsoft Marketplace accepts any OSI license) → Apache-2.0. - tools/docker/testui was ISC (npm init default) → Apache-2.0. CFC source-file SPDX headers under vendor/wheels/ and cli/lucli/ are out of scope here (1000+ files); see the issue for the deferred follow-up. Closes #2574 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * chore: replace vscode-ext LICENSE with Apache-2.0 text to match package.json (round 1) Addresses Reviewer A/B consensus from the convergence loop: the prior commit set tools/vscode-ext/package.json to "license": "Apache-2.0" but left tools/vscode-ext/LICENSE as MIT text, creating a contradiction between npm metadata and the on-disk license artifact (the very issue this PR set out to fix for SCA tooling). Picks Option A from A's review (replace LICENSE with Apache-2.0 text) because the PR's stated intent in the body is deliberate relicensing of this directory to align with the repo-wide Apache-2.0 — "MIT looked accidental." LICENSE now matches the root LICENSE byte-for-byte. The DCO-trailer finding on the earlier commit (8d58fb6) cannot be addressed by the bot without rewriting history (git commit --amend on a shared branch), which the bot's rails forbid. The original commit author needs to amend + force-push, or add a separate fixup. This new commit carries its own Signed-off-by via -s. Signed-off-by: claude[bot] <41898282+claude[bot]@users.noreply.github.com> --------- Signed-off-by: claude[bot] <41898282+claude[bot]@users.noreply.github.com> Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> Co-authored-by: claude[bot] <41898282+claude[bot]@users.noreply.github.com>
…tends (#2595) * fix(cli): wheels validate must strip CFML comments before checking extends The `validateModel()` / `validateController()` checks in `cli/lucli/services/Analysis.cfc` searched the raw file content for `extends="Model"` / `extends="Controller"`, so a commented-out declaration above an inheritance-less `component { … }` body satisfied the substring match and the validator reported success. Strip line, block, and tag-style CFML comments before the check, and add specs that pin the behavior for both models and controllers. Fixes #2491 Signed-off-by: claude[bot] <41898282+claude[bot]@users.noreply.github.com> * docs(web/guides): clarify wheels validate checks active (uncommented) extends declaration Signed-off-by: claude[bot] <41898282+claude[bot]@users.noreply.github.com> * fix(cli): address Reviewer A/B consensus findings (round 1) - AnalysisSpec.cfc: replace conditional expect() inside the CommentedAndReal.cfc loop with an explicit boolean flag pattern so the assertion runs unconditionally after the loop (no longer vacuous when the fix is working correctly). - AnalysisSpec.cfc: add a fifth `it()` covering the tag-style CFML comment path (`<!--- ... --->`), the first substitution in $stripCfmlComments() that previously had no spec. Signed-off-by: claude[bot] <41898282+claude[bot]@users.noreply.github.com> --------- Signed-off-by: claude[bot] <41898282+claude[bot]@users.noreply.github.com> Co-authored-by: claude[bot] <41898282+claude[bot]@users.noreply.github.com>
…cts (#2597) Apache 2.0 §4(a) requires LICENSE in every distributed artifact and §4(d) requires NOTICE to propagate to derivatives. PR #2593 covered the base-template artifact via prepare-base.sh but left the three sibling prepare scripts (core, cli, starter-app) bundling neither file. Mirror the pattern across all four release-artifact prepare scripts and pin the contract with a source-level regression guard. Fixes #2596 Signed-off-by: wheels-bot[bot] <wheels-bot[bot]@users.noreply.github.com> Co-authored-by: wheels-bot[bot] <wheels-bot[bot]@users.noreply.github.com> Co-authored-by: Peter Amiri <peter@alurium.com>
…orks (#2598) * fix(packages): use java.io.File.mkdirs() for cache dirs so Adobe CF works ManifestCache.$ensureDir used DirectoryCreate(path, true). The createPath flag is a Lucee extension — Adobe CF rejects the second argument and crashes the Tools → Packages page on every fresh ACF install with "The function takes 1 parameter" (#2567). Routes the recursive mkdir through java.io.File.mkdirs(), which has stable JVM-level semantics on Lucee, Adobe CF, and BoxLang. Mirrors the fix in the CLI-side ManifestCache so the two intentionally-paired files stay in sync. Adds regression coverage exercising a deeply-nested cache root (where parents don't exist yet — the case the createPath flag was meant to cover) and the on-first-use manifests subdirectory creation path. Also documents the BIF gotcha in .ai/wheels/cross-engine-compatibility.md so the next person reaches for Java NIO before tripping on it. Closes #2567 * fix(packages): throw typed exception when mkdirs() fails + add changelog entry Address Reviewer A on #2598: - $ensureDir now captures mkdirs()'s boolean return and throws a typed Wheels.Packages.CacheDir exception when creation actually failed. Without this guard, a permission/read-only-fs failure surfaced as a misleading downstream FileWrite error rather than a clear "could not create cache directory" diagnostic. The !DirectoryExists re-check swallows the benign concurrent-creation race where mkdirs() returns false because another worker won. - CHANGELOG entry under [Unreleased] → Fixed describing the user-facing symptom (Tools → Packages 500 on fresh ACF), root cause, and the cross-engine fix, mirroring the format of the surrounding entries.
…rroring (#2569) (#2600) * fix(view): replace cfhtmlhead with body-level redirect on /wheels/guides Adobe ColdFusion threw "Unable to add text to HTML HEAD tag" when loading the /wheels/guides redirect page. The wrapper view includes the layout header (which streams </head>) before the docs view runs, so by the time the docs view tried to inject a meta-refresh via cfhtmlhead the response had already moved past the head. Lucee tolerates this; Adobe does not. Drop cfhtmlhead and emit the redirect from the body: a hidden div holds the target in a data-url attribute (HTML-attribute-encoded with encodeForHTMLAttribute, matching the visible anchor in the same view), and a small inline script triggers window.location.href after 3 seconds. UX is unchanged on Lucee, and the page now renders cleanly on Adobe ColdFusion. Fixes #2569. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * docs(changelog): note /wheels/guides Adobe CF fix Address Reviewer A nit on PR #2600 by recording the cfhtmlhead → body redirect change under [Unreleased] > Fixed, mirroring the format used by recent entries. Signed-off-by: Peter Amiri <peter@alurium.com> * test(view): drop invented CLAUDE.md anchor in guidesRedirectHeadSpec Reviewer B caught that the Chr(60)-justification comment cited "CLAUDE.md 'Lucee Tag Scanner'" — that label came from a personal memory file, not the project CLAUDE.md, and no such section exists in the repo docs. Anchor the convention on a verifiable in-repo precedent (MigratorViewIconsSpec.cfc, which builds its <svg / <i icon patterns the same way) and explain the underlying "one bad CFC takes the whole suite down" failure mode without inventing a doc heading. Signed-off-by: Peter Amiri <peter@alurium.com> --------- Signed-off-by: Peter Amiri <peter@alurium.com> Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
… ASTcffunction (#2568) (#2599) Adobe CF's bytecode generator crashes with `java.lang.ArrayStoreException: coldfusion.compiler.ASTcffunction` when a function literal is passed as a named argument to a `new Component(...)` call. The error fires from `getComponentMetadata()` in TestBox, and because Adobe CF eagerly compiles every CFC in the bundle directory before running, the failure crashes the *entire* core test suite — every database (mysql/postgres/sqlserver/oracle/ cockroachdb/sqlite) returns HTTP 500 on adobe2023 and adobe2025. This is the same workaround already applied in SessionStrategySpec. The fix is mechanical: hoist each `keyFunction = function(req) { ... }` out of the constructor call into a `var keyFn = ...;` declaration above the `new wheels.middleware.RateLimiter(...)` call, then pass the reference. 12 sites updated. No behavior change on Lucee or BoxLang — they compile both forms identically. Also documents the trap in `.ai/wheels/cross-engine-compatibility.md` and the CLAUDE.md "Known cross-engine gotchas" list so future agents catch it before re-introducing it. Signed-off-by: Peter Amiri <peter@alurium.com> Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…ourceName-only configs (#2489) (#2602) * fix(cli): wheels test preamble stops doubling _test for coreTestDataSourceName-only configs (#2489) `$resolveAppTestDataSource` in `cli/lucli/Module.cfc` matched `config/settings.cfm` with `dataSourceName\s*=\s*"([^"]*)"`. The case-insensitive matcher caught the trailing substring inside `set(coreTestDataSourceName="testappdb_test")` and re-appended `_test`, surfacing `testappdb_test_test` in the `Running app tests (...)` preamble. The matcher now uses `\bdataSourceName\b` and strips CFML comments first (matching the pattern already used by `info()`), and guards against re-appending `_test` if the resolved base already ends in `_test`. .env lookup is also anchored to start-of-line so an unrelated key whose name happens to end in `DATASOURCE_NAME` can't match by accident. Issue thread also reported `--filter` still being ignored on the latest snapshot. The CLI's `$normalizeTestFilter` + URL build trace correctly through `app-runner.cfm`'s `?directory=` regex (verified end-to-end against a local server: `--filter=models` correctly scopes to `tests.specs.models`). Most likely the user hit a stale CLI install pre-#2394. Extracted the app-runner's `?directory=` regex into `TestDirectoryResolver` alongside `TestDbResolver` so the silent-fallback path (bare `?directory=models` collapsing to `tests.specs`) is unit-testable instead of HTTP-only — if the regex ever regresses, a unit test will catch it before it ships. Also exposes `$resolveAppTestDataSource` as `public` (matching the `$`-prefix-for-internal convention used by `$normalizeTestFilter`) so the new regression specs can drive it directly. * fix(cli): address Reviewer A/B consensus findings (round 1) - Add regression spec for the `.env` start-of-line anchor in $resolveAppTestDataSource (cli/lucli/Module.cfc:5072): confirms a key whose name ends in DATASOURCE_NAME (`MY_DATASOURCE_NAME=...`) does not false-match against the DATASOURCE_NAME extraction. cli/lucli/tests/specs/commands/TestCommandSpec.cfc. Closes A's one coverage-gap action item; B aligned in round 1. Signed-off-by: claude[bot] <41898282+claude[bot]@users.noreply.github.com> --------- Signed-off-by: claude[bot] <41898282+claude[bot]@users.noreply.github.com> Co-authored-by: claude[bot] <41898282+claude[bot]@users.noreply.github.com>
…ation property (#2412) (#2601) * fix(model): clear error when struct/array mass-assigned to non-association property (#2412) When `model.new(params.user)` or `setProperties()` was called with a struct or array value for a property that wasn't declared as a nested association, `$setProperty` fell through to `this[property] = value` and silently overwrote the model attribute with the bad shape. The cast error then fired much later — inside a user callback like `beforeValidation("normalizeEmail")` doing `LCase(this.email)` — and Lucee reported "Can't cast Complex Object Type Struct to String" pointing at the user's code instead of at the upstream form-data mismatch. The most common trigger is a curl POST whose body uses bracket-nested keys without an `=` separator: --data-urlencode "user[email][test@example.com]" ^ no `=` here ^ Lucee's form parser reads the trailing `[test@example.com]` as a deeper nested-struct path, so `params.user.email` arrives as `{"test@example.com": ""}` instead of a string. Browsers always %-encode `@` and include the `=`, which is why the rendered form works. Fix: when `$setProperty` reaches the assignment branch and the value is still a struct or array (i.e. no `hasOne`/`hasMany`/`belongsTo` with `nestedProperties()` matched it), throw `Wheels.PropertyIsIncorrectType` with a message naming the model and property plus extendedInfo describing the likely root cause. Legitimate nested-attribute assignments are unaffected because they match the existing branches above the new guard. Also corrects the chapter 6 tutorial gotcha note: the failure mode is the missing `=` separator, not `@` encoding per se. The previous text claimed `user[email]=alice@example.com` was enough to trigger the bug, which contradicts the framework's existing `createParamsSpec.cfc`. Note: local `tools/test-local.sh` could not run from this worktree because another worktree already held the wheels CLI's "wheels" server-name lock. CI is authoritative — the new `propertiesSpec.cfc` describe block exercises the four shapes (struct + non-assoc, array + non-assoc, struct + nested-assoc happy path, scalar happy path). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(model): scope #2412 guard to real DB columns to avoid breaking hasMany/control-param paths CI on the first push surfaced 21 regressions across crud, nested- properties, association, and bulk-operations specs. Two patterns: 1. Arrays of model instances being assigned to `hasMany` properties that don't have `nestedProperties()` enabled (e.g. `author.posts = [postObj1, postObj2]` after `findOne(include="posts")`). The original `$setProperty` skips its nested-collection branch when the array contains objects (`!IsObject(arguments.value[1])` check), so the array fell through to direct assignment — which is legitimate for loaded associations. 2. Framework-internal control parameters like `useIndex` (a struct of model→index-name mappings) leaking through `$setProperties` along with finder/calculation arguments. These aren't model properties at all, just args that happen to pass through the same path. Both shapes hit the new guard's `IsStruct || IsArray` check and threw, even though neither is the bug we're trying to catch. Narrow the guard to fire only when the property is a real scalar database column — i.e. `StructKeyExists(variables.wheels.class.properties, arguments.property)`. `class.properties` is populated from schema introspection at class init and contains only actual DB columns (not associations, not transient args). The original bug case (`email` on a User model) still hits the guard; everything else falls through to the existing assignment branch unchanged. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(model): address Reviewer A/B consensus findings (round 1) - Trim multi-line comment block in $setProperty to one short line (N1). - Add regression specs for empty array and array-of-objects assigned to a hasMany property with nestedProperties() enabled, confirming the narrowed guard at properties.cfc:580 falls through for both shapes (T1, A's findings for #2601). - Fix tutorial curl example: the previous illustration used `--data-urlencode "user[email][alice@example.com]"`, but curl reads any unescaped `@` in such an argument as the start of a filename (the `name@filename` form), so the example would error on disk before ever reaching Lucee. Switch to a non-`@` bracket key and call out the curl-level pitfall (B's missed-issue addendum). C1 and C2 (the empty-array and array-of-objects regressions on hasMany+nestedProperties) were already addressed by 6bae0e6, which narrowed the guard to fire only when the property is a real scalar DB column. The two new specs lock that behaviour against future regressions. CM1 (Signed-off-by trailer missing on b840c51 and 6bae0e6) cannot be retroactively added by this bot stage — `.claude/commands/_shared-rails.md` forbids any history-rewriting git subcommand. This commit is signed off; the prior two will need a human-driven amend/rebase or a squash merge to satisfy the DCO check. Signed-off-by: claude[bot] <41898282+claude[bot]@users.noreply.github.com> * fix(model): address Reviewer A/B consensus findings (round 2) - vendor/wheels/tests/specs/model/propertiesSpec.cfc: trim two multi-line comment blocks to single lines per CLAUDE.md ("never write multi-line comment blocks — one short line max"). The 8-line context block at the top of the new describe and the 3-line fixture explanation inside the "still accepts struct values for nested associations" it() are both replaced with one-line references; the issue #2412 background is already in the PR description and commit messages. - CHANGELOG.md: replace the curl example `--data-urlencode "user[email][test@example.com]"` with `--data-urlencode "user[email][badkey]"`. The `@` form would have failed at the curl client (curl reads any unescaped `@` in `--data-urlencode` as the start of a filename, the `name@filename` form), so the example could not actually trigger the Lucee parser behavior the entry is describing. Matches the tutorial correction already applied in this PR. Reviewer-flagged DCO trailers missing from b840c51 and 6bae0e6 remain a human-action item: rewriting published history is forbidden by .claude/commands/_shared-rails.md, so the PR author needs to run `git rebase --signoff develop && git push --force-with-lease` or the PR can be merged via "Squash and merge" so the resulting commit carries one DCO trailer. Signed-off-by: claude[bot] <41898282+claude[bot]@users.noreply.github.com> * fix(test): escape `#` in propertiesSpec it() descriptions (Lucee tag scanner crash) CI from the most recent push returned HTTP 500 with "Invalid Syntax Closing [#] not found" at propertiesSpec.cfc:437. The two new specs added by 72effe4 and shipped through cc7ecaf embed the literal PR reference `#2601` in their `it("... regression for #2601 review")` description STRINGS — and CFML reads `#` inside a string literal as the start of an `#expression#` interpolation. The scanner cannot find the closing `#`, the whole CFC fails to compile, and the test runner crashes for the entire model bundle (taking 800+ specs down). CLAUDE.md ("Lucee Tag Scanner" memory + the "`#` escape gotcha" call- out in the testing section) documents this exact trap. Comments escape the scanner because `//` lines are not evaluated, which is why the trimmed comment blocks in cc7ecaf still containing `#2412` were fine — but the strings inside `it(...)` calls are not comments. Escape both occurrences as `##2601` so the `#` survives as a literal. Reproduced and verified locally against this worktree's Lucee 7 + SQLite server: 810 model specs pass (3445 total core), 0 failures, 0 errors (the 12 remaining errors come from `BrowserLoginSpec` / `BrowserRouteSpec` needing `wheels browser setup` and are unrelated to this PR). Signed-off-by: Peter Amiri <peter@alurium.com> --------- Signed-off-by: claude[bot] <41898282+claude[bot]@users.noreply.github.com> Signed-off-by: Peter Amiri <peter@alurium.com> Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> Co-authored-by: claude[bot] <41898282+claude[bot]@users.noreply.github.com>
…succeeds (#2603) Scoop's install order is pre_install -> bin shim creation -> post_install. Both manifests emitted the wheels.cmd launcher in post_install, so the shim step ran first, failed with `Can't shim 'wheels.cmd': File doesn't exist.`, and aborted the install before the launcher was ever written. Surfaced on Windows as a permanently-broken `scoop install wheels-be` (and would have hit `wheels` GA the same way). Move the launcher-creation block to pre_install in the generator. Validator and README pick up the same rename. The emitted JSON manifests change by exactly one key per file; everything else (wrapper content, URLs, hashes, extract paths, banners) is byte-identical. Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…b/.rpm install (#2604) * docs(web/guides): replace nonexistent install.sh URL with working .deb/.rpm install The Linux tabs on the start-here installing guide and the upgrade flow pointed at `https://get.wheels.dev/install.sh` — a URL that was planned for the Phase 2 universal installer but doesn't exist yet. Users on Linux hit a 404 and a confusing `bash: command not found` chain. Rewrite all three install-related guides to use the .deb/.rpm packages that release.yml already produces (verified on snapshot v4.0.0-snapshot.1825). The new snippets resolve the latest version dynamically via the GitHub API, so they keep working as releases roll over. - start-here/installing.mdx: Linux install + upgrade tabs; troubleshooting no longer references the never-existed `~/.wheels/env` file. - command-line-tools/installation.mdx: split the "macOS and Linux (Homebrew)" section — Linux now has its own `.deb`/`.rpm` section with the package layout and an Aside flagging native apt/yum repos as a v4.0.x follow-up. - start-here/release-channels.mdx: replace literal `<version>` placeholders with the API-driven version resolution; extend the "Switching channels" section beyond Homebrew (Linux uses Conflicts:/Replaces: metadata, Scoop refuses dual install). Phase 2 (native apt.wheels.dev / yum.wheels.dev repositories) is planned as a follow-up — see tools/distribution-drafts/linux-packages/README.md for the full plan. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> Signed-off-by: Peter Amiri <peter@alurium.com> * docs(web/guides): fix bot-flagged issues on Linux install/upgrade/switch docs Wheels-bot's Reviewer A + Reviewer B caught five issues with the previous revision of #2604. All five address the same root tension: the docs read as if the BE channel mirrors the stable channel filename-for-filename and API-call-for-API-call, but the underlying release pipeline (release.yml hardcoding CHANNEL=stable, GitHub /releases/latest excluding pre-releases) makes the asymmetry user-visible. 1. installing.mdx Aside (bot finding 1) — the "substitute repo name in the URLs above" prose pointed at /releases/latest, which returns nothing against the pre-release-only snapshots repo. WHEELS_VERSION would be empty and the download URL would 404 silently. Replaced with two self-contained snippets (Debian/Ubuntu + Fedora/RHEL) using /releases + head -1, matching the pattern already used in release-channels.mdx. 2. release-channels.mdx Linux switch block (bot finding 2) — the previous block was comments-only with no executable command, and referenced "wheels-be" as a filename component that doesn't exist (release.yml hardcodes CHANNEL=stable, so both stable and snapshot publish a single package literally named `wheels`). Replaced with concrete stable → bleeding-edge and bleeding-edge → stable command blocks. Noted the --allow-downgrades requirement for BE → stable on apt (4.0.0 sorts below 4.0.1~snapshot.500 per dpkg). 3. release-channels.mdx comment on the version substitution (bot finding 3) — the inline comment said "GitHub Releases rewrites ~ to ." but the shell substitution is /-/. The reader had to chain three concepts themselves (tag uses hyphen → internal package version uses tilde → GitHub mangles tilde to dot at upload). Expanded the comment to spell the chain out explicitly. 4. installing.mdx step 1 (bot finding 4) — the manual `sudo apt install openjdk-21-jre-headless` step contradicted both the intro paragraph ("depends on OpenJDK 21") and installation.mdx ("apt/dnf pulls it in alongside wheels"). Dropped step 1 entirely; reworded the intro to spell out the auto-pull behavior explicitly. Step 2 (install) and step 3 (verify) renumber to 1 and 2. 5. installation.mdx BE prose (Reviewer B's missed-issue finding) — same root cause as #1, in the deeper reference doc. Same fix: replace the "substitute the repo name" prose with two self-contained BE snippets using /releases + head -1. Also added a brief BE pointer to the Linux upgrade tab, mirroring the "# or, for the bleeding-edge channel:" line in the Windows upgrade tab. Verified the BE snippet end-to-end: SNAP_TAG=$(curl ... /repos/wheels-dev/wheels-snapshots/releases | ... | head -1) → 4.0.0-snapshot.1826 → URL: .../v4.0.0-snapshot.1826/wheels_4.0.0.snapshot.1826_amd64.deb → HTTP 302 (redirect to release-assets.githubusercontent.com) Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> Signed-off-by: Peter Amiri <peter@alurium.com> * docs(web/guides): fill remaining gaps on Linux switch and Scoop reverse paths Round-2 bot findings: 1. release-channels.mdx Switching channels — the Linux blocks named dnf in their comments but only showed apt commands, leaving Fedora/RHEL users without a usable command. Split each direction into a Debian/Ubuntu block and a Fedora/RHEL block. For the BE → stable direction, dnf gets `dnf downgrade <url>` (the rpm-native way to handle an older-version transition); apt keeps its --allow-downgrades trick. 2. release-channels.mdx Switching channels — Scoop only had stable → BE; added the symmetric bleeding-edge → stable block to match the macOS pair. 3. Trust-model documentation — Phase 1 ships .deb/.rpm without detached GPG signatures, so download integrity depends solely on GitHub's HTTPS transport. Added a `<Aside type="caution">` on both installing.mdx (the onboarding guide) and installation.mdx (the deeper reference) noting this explicitly and pointing forward to the Phase 2 signed repos at apt.wheels.dev / yum.wheels.dev. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> Signed-off-by: Peter Amiri <peter@alurium.com> * docs(web/guides): fill diagonal gap in Installing each channel Linux tab Round-3 bot finding: the "Installing each channel" Linux tab in release-channels.mdx showed only two of four distro × channel combinations — Debian/Ubuntu (stable) with apt, and Fedora/RHEL (bleeding-edge) with dnf. The diagonal pairing left a Fedora user looking for the stable rpm install or a Debian user looking for the bleeding-edge .deb install with no command they could copy out of this section. Added the two missing snippets to make the full 2×2 (apt/dnf × stable/bleeding-edge) explicit, mirroring the structure already established in the round-2 "Switching channels" section. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> Signed-off-by: Peter Amiri <peter@alurium.com> --------- Signed-off-by: Peter Amiri <peter@alurium.com> Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* chore: prep CHANGELOG for v4.0.0 GA - Promote [Unreleased] heading to # [4.0.0] with GitHub release link and date 2026-05-11 (replace `=> TBD` so release.yml validation passes). - Update PR count (185 → 260+) and contributor list (+ @MukundaKatta) to match docs/releases/wheels-4.0-audit.md. - Add new "Distribution" subsection under Added — captures the four install channels (Homebrew, Scoop, .deb/.rpm, WinGet drafts) that landed in 4.0 but weren't itemized in the changelog. - Fix the section terminator after 4.0.0's notes from `----` (4 dashes) to `---` (3 dashes) so release.yml's release-notes awk extraction (`awk '/^# \[VERSION\]/,/^---$/'`) terminates at the right boundary instead of running into the 3.0.0 entry. wheels.json is already at "4.0.0" (no snapshot suffix), so the release.yml clean-source-version check passes. First-party packages all declare wheelsVersion >=4.0 so no coordination updates are needed. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> Signed-off-by: Peter Amiri <peter@alurium.com> * docs(changelog): add Fixed entry for #2603 Scoop pre_install fix Captures the user-visible impact ("scoop install fails with Can't shim wheels.cmd") and the actual fix (move launcher emit from post_install to pre_install, where Scoop's install order requires it). Already merged on develop as commit e7445bc; this just makes sure the GA changelog reflects it under v4.0.0's Fixed section. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> Signed-off-by: Peter Amiri <peter@alurium.com> * docs(changelog): address Reviewer A/B consensus findings (round 1) - Fix Homebrew tap link text from `wheels-dev/wheels` to `wheels-dev/homebrew-wheels` so the visible repo name matches the URL target (CHANGELOG.md:104). - Bump the 4.0.0 release date marker from `2026-05-11` to `2026-05-12` so it reflects today's tag day (CHANGELOG.md:21). Not addressed in this commit: - Missing `Signed-off-by:` trailers on `bff31f981` and `d971cde8e`. Adding them requires `git commit --amend -s` (or interactive rebase) plus a force-push, both of which the wheels-bot rails forbid. Flagged to the human in the address-review comment so a maintainer can amend the trailer chain before the FF-merge to main. Signed-off-by: claude[bot] <41898282+claude[bot]@users.noreply.github.com> Signed-off-by: Peter Amiri <peter@alurium.com> --------- Signed-off-by: Peter Amiri <peter@alurium.com> Signed-off-by: claude[bot] <41898282+claude[bot]@users.noreply.github.com> Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> Co-authored-by: claude[bot] <41898282+claude[bot]@users.noreply.github.com>
Merging the develop branch into main for the 4.0.0 GA cut. Main carried six commits from the 3.0.x hotfix line (#1937 and ancestors, throwOnColumnNotFound backport and friends) that diverged from develop, so the standard `merge --ff-only` flow doesn't apply. Using `--no-ff -X theirs` so: - The merge commit preserves both branch histories on main. - On any file-level conflict, develop's version wins (correct — develop is the canonical state of the 4.0 line; main's 3.0.x throwOnColumnNotFound work has been re-landed on develop via PR #1938 and is captured in the 4.0.0 CHANGELOG). Pre-release archive branches preserve both prior tips: - archive/develop-pre-v4.0.0 → 3914c90 - archive/main-pre-v4.0.0 → 858bfb4 Signed-off-by: Peter Amiri <peter@alurium.com> # Conflicts: # core/src/wheels/Public.cfc # core/src/wheels/events/onapplicationstart.cfc # core/src/wheels/public/docs/guides.cfm # core/src/wheels/public/docs/layouts/guides.cfm # docs/src/SUMMARY.md # docs/src/command-line-tools/commands/database/dbmigrate-info.md # docs/src/command-line-tools/commands/database/dbmigrate-latest.md # docs/src/command-line-tools/commands/plugins/plugins-search.md # templates/base/src/tests/runner.cfm # vendor/wheels/public/docs/DocsHelper.cfc # web/sites/guides/src/content/docs/v3-0-0/command-line-tools/commands/generate/code.md
This was referenced May 12, 2026
This was referenced May 20, 2026
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.
TL;DR
Cut the 4.0.0 GA. This PR carries a single merge commit (
d7a6b9a68) that brings all ofdevelopintomain. When this PR lands,release.ymlfires on the resulting push to main, builds the GA artifacts, creates thev4.0.0tag, and publishes the GitHub Release.Why a merge commit instead of fast-forward
mainanddevelopdiverged when 3.x-line hotfixes (#1937and ancestors — thethrowOnColumnNotFoundwork) landed on main but were re-implemented independently on develop (CHANGELOG cites#1938for the 4.0 version of the same feature). Last common ancestor was the v3.0.0 cut on 2026-01-10.That divergence means
git merge --ff-onlyis structurally impossible. The merge commit ond7a6b9a68was constructed withgit merge --no-ff -X theirs origin/develop, then the tree was reset to exactly match develop's tree for the handful of paths where 3-way merge took content from main's 3.x hotfix history that wasn't part of the planned 4.0 scope. The final tree is byte-identical to develop's at3914c9080.Pre-release archives preserve both tips:
archive/develop-pre-v4.0.0→3914c9080archive/main-pre-v4.0.0→858bfb430Pre-flight checks (all passing)
# [4.0.0]...=> 2026-05-12(no=> TBD) —release.ymlvalidation passeswheels.jsonversion is"4.0.0"(no-snapshotsuffix)package.jsonversion matches at"4.0.0"wheelsVersion >=4.0origin/developexactly (git diff origin/developreturns nothing)Signed-off-by: Peter Amiri <peter@alurium.com>How to merge this PR
Use "Create a merge commit" (not squash, not rebase). This preserves
d7a6b9a68's structure as a parent of main HEAD, keeping develop's history reachable from main. Squash would collapsed7a6b9a68into a single new commit and lose the parentage link; rebase would replay onto main and conflict with the existing merge structure.The resulting main HEAD will be a new merge commit whose first parent is
858bfb430(main's current HEAD) and second parent isd7a6b9a68(this branch's HEAD). The tree of both will be identical to develop's tree.What fires after merge
release.ymltriggers on the push to main=> TBDcheck) andwheels.json(no-snapshotsuffix) — both already passwheels-core,wheels-cli,wheels-base-template,wheels-starter-app,wheels-module,.deb,.rpmv4.0.0tag at main HEAD viasoftprops/action-gh-releasewheels-releasedtohomebrew-wheelsandscoop-wheelsrepos → their auto-bump workflows open PRs within ~5 minbump-develop-version.ymlopens a PR against develop bumpingwheels.jsonto the next-patch baseline (likely4.0.1)Post-merge smoke test
Per
.github/RELEASE_PLAYBOOK.md:Bypass note
A direct
git push origin mainwas attempted first (the playbook flow) but rejected by themainruleset ("Changes must be made through a pull request."). This PR is the channel through which the ruleset wants the merge to land. The tag was manually pre-pushed and then deleted;release.ymlwill recreate it during the post-merge build.