Skip to content

Commit 44e9249

Browse files
authored
Merge pull request #2769 from wheels-dev/release/v4.0.1-to-main
Release 4.0.1 — merge develop into main
2 parents 91e7398 + ca4ee38 commit 44e9249

1,041 files changed

Lines changed: 54587 additions & 3556 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.ai/wheels/cross-engine-compatibility.md

Lines changed: 221 additions & 0 deletions
Large diffs are not rendered by default.

.ai/wheels/deploy.md

Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
# Deploy Reference
2+
3+
`wheels deploy` ships a Dockerized Wheels app to production Linux servers via SSH. Ported from Basecamp Kamal's developer CLI — same `config/deploy.yml` schema, same on-server conventions (container names, labels, network, lock path), invokes the same `kamal-proxy` Go binary for zero-downtime rollover. No Ruby runtime required.
4+
5+
## Commands
6+
7+
```
8+
wheels deploy init # scaffold config/deploy.yml + .kamal/secrets
9+
wheels deploy setup # one-time server bootstrap + first deploy
10+
wheels deploy # rolling deploy
11+
wheels deploy --dry-run # print commands without executing
12+
wheels deploy rollback v1 # roll back to a previous version
13+
wheels deploy config # print resolved config as YAML
14+
wheels deploy version # show Kamal version this port mirrors
15+
```
16+
17+
## Subcommands
18+
19+
```
20+
wheels deploy app <verb> # boot/start/stop/details/containers/images/logs/live/maintenance/remove
21+
wheels deploy proxy <verb> # boot/reboot/start/stop/restart/details/logs/remove
22+
wheels deploy accessory <verb> # boot/reboot/start/stop/restart/details/logs/remove (sidecars: db/redis/search)
23+
wheels deploy build <verb> # deliver/push/pull/create/remove/details/dev
24+
wheels deploy registry <verb> # setup/login/logout/remove
25+
wheels deploy bootstrap # install Docker on every host (flat alias — preferred)
26+
wheels deploy exec "<cmd>" # run a command on every host (flat alias — preferred)
27+
wheels deploy server <verb> # exec/bootstrap (legacy nested form — see #2677)
28+
wheels deploy prune <verb> # all/images/containers [--keep=N]
29+
wheels deploy lock <verb> # acquire/release/status (manual — normal deploys auto-lock)
30+
wheels deploy fetch-secrets ... # resolve KEY=VALUE lines from an adapter (flat alias — preferred)
31+
wheels deploy extract-secrets # pull one key from a KEY=VALUE block (flat alias — preferred)
32+
wheels deploy print-secrets # print resolved .kamal/secrets (flat alias — preferred)
33+
wheels deploy secrets <verb> # fetch/extract/print (legacy nested form — see #2697)
34+
wheels deploy audit # tail /tmp/kamal-audit.log on each server
35+
wheels deploy details # aggregate app + proxy + accessory status
36+
wheels deploy remove --confirm # teardown all app/proxy/accessory containers
37+
wheels deploy docs [section] # in-terminal config reference
38+
```
39+
40+
## On-server parity contract (byte-compatible with Ruby Kamal)
41+
42+
- Container names: `<service>-<role>-<version>`
43+
- Labels: `service=`, `role=`, `destination=`, `version=`
44+
- Docker network: `kamal`
45+
- Lock file: `/tmp/kamal_deploy_lock_<service>`
46+
- Proxy config: `/home/<user>/.config/kamal-proxy/`
47+
- Hook env prefix: `KAMAL_*` (never `WHEELS_*` — user hooks migrate unchanged)
48+
49+
A server managed by Ruby Kamal can be taken over by `wheels deploy` without cleanup.
50+
51+
## Architecture
52+
53+
```
54+
cli/lucli/services/deploy/
55+
├── cli/*.cfc DeployMainCli + Deploy<App|Proxy|Accessory|Build|Registry|Server|Prune|Lock|Secrets>Cli
56+
├── commands/*.cfc Base + Docker/App/Proxy/Builder/Registry/Auditor/Lock/Hook/Accessory/PruneCommands
57+
├── config/*.cfc Config + Role/Env/Builder/Proxy/Registry/Ssh/Accessory/Validator/ConfigLoader
58+
├── lib/*.cfc JarLoader/Mustache/Yaml/SshClient/SshPool/FakeSshPool/Output/SecretResolver
59+
└── secrets/*.cfc BaseAdapter + OnePassword/Bitwarden/AwsSecrets/LastPass/Doppler adapters
60+
61+
cli/lucli/lib/deploy/*.jar jmustache, snakeyaml, sshj + BouncyCastle transitives (URLClassLoader-isolated)
62+
cli/lucli/templates/deploy/ Mustache templates for `wheels deploy init` output
63+
```
64+
65+
Commands-are-strings invariant: every `*Commands.cfc` method returns a shell-command string; only `*Cli.cfc` and the orchestrator execute them. That's why `--dry-run` is trivial and unit tests run without network.
66+
67+
## Critical gotchas
68+
69+
1. **Kamal-compatible schema, ONE divergence.** ERB in `deploy.yml` is NOT supported (rendering it would require embedding a Ruby runtime). Kamal's native `${VAR}` env-var interpolation is preserved unchanged — uppercase-snake tokens resolve via `envOverride → .kamal/secrets → System.getenv → ""` (see `ConfigLoader.$interpolate`). Mustache (`{{...}}`) is used only by `wheels deploy init` to scaffold a fresh `deploy.yml`/`secrets`; it is NOT applied to `deploy.yml` at runtime. Everything else in `config/deploy.yml` is byte-identical to Kamal 2.4.0.
70+
2. **Hook env prefix is `KAMAL_`, not `WHEELS_`.** Deliberate — Ruby Kamal users' existing `.kamal/hooks/` scripts work unchanged.
71+
3. **`app live` / `app maintenance` use a marker file** (`/tmp/kamal-maintenance-<svc>`) rather than kamal-proxy native maintenance mode. Phase 2 simplification; Phase 3 follow-up will align with Kamal's proxy-native semantics.
72+
4. **`wheels deploy remove` is destructive and requires `--confirm`.** Bare `wheels deploy remove` throws without touching anything.
73+
5. **Lucee reserved scope names in subagent-authored deploy code.** `client`, `session`, `application` — use `ssh`/`sc`, `sess`, `app` instead. Bit us multiple times during the port.
74+
6. **No `--dry-run` flag in Ruby Kamal 2.4.0.** The `tools/deploy-config-diff.sh` harness compares config-layer output only. Byte-identical command-string parity is aspirational; see `tools/deploy-dry-run-diff.sh` for the plan.
75+
7. **`wheels deploy server <verb>` collides with LuCLI's top-level `server` command.** LuCLI (the picocli runtime under the wheels brand) registers `server` for Lucee dev-server lifecycle, so picocli grabs the `server` token before it can reach the deploy dispatcher. The wheels module exposes flat aliases `wheels deploy bootstrap` and `wheels deploy exec` that sidestep the collision — these are the canonical CLI form. The nested `server <verb>` branch is retained in `Module.cfc::deploy()` for MCP/programmatic callers. See [#2677](https://github.com/wheels-dev/wheels/issues/2677).
76+
8. **`wheels deploy secrets <verb>` collides with LuCLI's top-level `secrets` command.** Same shape as #2677 — LuCLI registers `secrets` for its own credential store (init/set/list/rm/get/provider). The wheels module exposes flat aliases `wheels deploy fetch-secrets`, `wheels deploy extract-secrets`, and `wheels deploy print-secrets` that sidestep the collision — these are the canonical CLI form. The nested `secrets <verb>` branch is retained for MCP/programmatic callers. See [#2697](https://github.com/wheels-dev/wheels/issues/2697).
77+
78+
## Testing
79+
80+
`cli/lucli/tests/specs/deploy/` extends `wheels.wheelstest.system.BaseSpec`. Run with:
81+
82+
bash tools/test-cli-local.sh
83+
84+
Fixtures at `cli/lucli/tests/_fixtures/deploy/configs/` (`minimal.yml`, `full.yml`, `with-accessories.yml`, `invalid/*.yml`). `FakeSshPool.cfc` records every command for offline assertions; no sshd needed for unit tests. `SshClientSpec` + `SshPoolSpec` exercise real SSH via the fixture at `cli/lucli/tests/_fixtures/deploy/sshd/` (brought up by `tools/deploy-sshd-up.sh`).
85+
86+
## Reference docs
87+
88+
- User guides: `web/sites/guides/src/content/docs/v4-0-0/deployment/` (first-deploy, production-config, accessories, secrets, hooks, migrating-from-kamal, security-hardening, docker-deployment)
89+
- In-source CLI docs: `cli/lucli/services/deploy/cli/docs/` (per-verb)
90+
- Design spec: [docs/superpowers/specs/2026-04-20-wheels-deploy-kamal-port-design.md](../../docs/superpowers/specs/2026-04-20-wheels-deploy-kamal-port-design.md)
91+
- Implementation plan: [docs/superpowers/plans/2026-04-20-wheels-deploy-kamal-port.md](../../docs/superpowers/plans/2026-04-20-wheels-deploy-kamal-port.md)
92+
- Retrospective: [docs/superpowers/plans/2026-04-21-phase1-retrospective.md](../../docs/superpowers/plans/2026-04-21-phase1-retrospective.md)
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
# Browser Testing
2+
3+
Shipped in v4.0 across PRs #2113, #2115, #2116. Specs extend `wheels.wheelstest.BrowserTest` and drive a real Chromium through `this.browser` — a fluent DSL wrapping Playwright Java.
4+
5+
## Example
6+
7+
```cfm
8+
// vendor/wheels/tests/specs/browser/LoginBrowserSpec.cfc
9+
component extends="wheels.wheelstest.BrowserTest" {
10+
11+
this.browserEngine = "chromium"; // chromium only in PR 1
12+
13+
function run() {
14+
// browserDescribe() wraps describe() with beforeEach/afterEach that
15+
// create a fresh Page per `it`. WheelsTest's BDD lifecycle only treats
16+
// beforeAll/afterAll as class-level, so we register per-it hooks
17+
// from inside the suite body via this helper.
18+
browserDescribe("Login flow", () => {
19+
it("can load a page and read its title", () => {
20+
if (this.browserTestSkipped) return;
21+
this.browser.visitUrl("data:text/html,<title>Hi</title><h1>x</h1>")
22+
.assertTitleContains("Hi");
23+
});
24+
});
25+
}
26+
}
27+
```
28+
29+
## Installation
30+
31+
Install Playwright locally before first run (~370MB download: JARs + Chromium):
32+
33+
```bash
34+
wheels browser setup # downloads JARs + Chromium
35+
```
36+
37+
Then run browser specs via the normal test suite:
38+
39+
```bash
40+
bash tools/test-local.sh # skips browser specs if JARs missing
41+
```
42+
43+
## Implemented DSL methods
44+
45+
- **Navigation:** visit, visitUrl, back, forward, refresh, visitRoute
46+
- **Interaction:** click, press, fill, type, clear, select, check, uncheck, attach, dragAndDrop
47+
- **Keyboard:** keys, pressEnter, pressTab, pressEscape
48+
- **Waiting:** waitFor, waitForText, waitForUrl
49+
- **Scoping:** within(selector, callback)
50+
- **Cookies:** setCookie, deleteCookie, cookie, clearCookies
51+
- **Auth:** loginAs, logout
52+
- **Dialogs:** acceptDialog, dismissDialog, dialogMessage (Lucee-only via createDynamicProxy)
53+
- **Viewport:** resize, resizeToMobile, resizeToTablet, resizeToDesktop
54+
- **Script:** script (returns `page.evaluate` result), pause
55+
- **Assertions (text/vis/presence):** assertSee, assertDontSee, assertSeeIn, assertVisible, assertMissing, assertPresent, assertNotPresent
56+
- **Assertions (URL/title/query):** assertUrlIs, assertUrlContains, assertTitleContains, assertQueryStringHas, assertQueryStringMissing, assertRouteIs
57+
- **Assertions (form):** assertInputValue, assertChecked, assertHasClass
58+
- **Terminals:** currentUrl, title, pageSource, text, value, screenshot
59+
60+
## Key gotchas
61+
62+
- **`##` in selectors** — CFML requires `##` to emit literal `#`. `"##email"``"#email"` at runtime.
63+
- **`client` is a Lucee reserved scope.** `var client = ...` in a closure throws "client scope is not enabled". Use `var c = ...` or `var bc = ...`. (Generalized rule: see CLAUDE.md anti-pattern #11.)
64+
- **Data URLs work for most tests** — no server needed for ~95% of DSL coverage. Full HTTP integration (cookies, form submits, redirects) needs a running fixture app; that wiring is the same as Wheels Web app bootstrap (separate server + baseUrl).
65+
- **`this.browserTestSkipped`** — when Playwright JARs aren't installed (fresh CI, clean machine), `beforeAll` sets this flag and `browserDescribe`'s hooks short-circuit. All `it`s should check `if (this.browserTestSkipped) return;` to stay green on CI.
66+
- **CI runs browser tests**`pr.yml` and `snapshot.yml` install Playwright JARs + Chromium (cached via `browser-manifest.json` hash). Browser specs run as part of the normal test suite. `WHEELS_BROWSER_TEST_BASE_URL=http://localhost:60007` is set automatically.
67+
- **Fixture routes**`/_browser/login-as` and `/_browser/logout` are mounted automatically in test mode. They must come before `.wildcard()` in routes.cfm. In the Routes UI (`/wheels/routes`) all `/_browser/*` routes appear under the **Internal** tab, not Application.
68+
- **Dialogs are Lucee-only**`acceptDialog`, `dismissDialog`, `dialogMessage` use `createDynamicProxy` which is Lucee-specific. Specs skip gracefully on other engines.

.ai/wheels/wheels-bot.md

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
# Wheels Bot
2+
3+
`wheels-bot[bot]` is a custom GitHub App that runs Claude-powered automation on issues and PRs in `wheels-dev/wheels`. Five stages, all opt-out via the `[skip-claude]` label or repo variable `WHEELS_BOT_ENABLED=false`. Slash-command prompts live in `.claude/commands/`; workflows in `.github/workflows/bot-*.yml`. Full user-facing docs: [`docs/contributing/wheels-bot.md`](../../docs/contributing/wheels-bot.md).
4+
5+
## Stages
6+
7+
| Stage | Trigger | Model | Output |
8+
|---|---|---|---|
9+
| Triage | issue opened/reopened | Opus | Comment classifying as `bug` / `framework-design` / `other` (+ confidence on `bug` path). Reads code with the allowlisted tools to resolve uncertainty before rating. |
10+
| Research | bot triage emits `framework-design` marker | Opus | Comment comparing Rails / Laravel / Django / Phoenix / Spring Boot / +1 and recommending a Wheels-idiomatic path (+ confidence). |
11+
| Propose Fix | bot triage emits `triage-confidence:high\|medium` OR research emits `research-confidence:high\|medium` (or `workflow_dispatch`) | Opus | TDD-mandatory draft PR on branch `fix/bot-<issue>-<slug>`. Spec-then-implementation, both required by `bot-tdd-gate.yml`. |
12+
| Reviewer A | PR opened / synchronized / ready_for_review | Sonnet | Single PR review with line comments, verdict, and `wheels-bot:review-a:<pr>:<sha>` marker. |
13+
| Reviewer B | Reviewer A submits a review | Sonnet | PR comment critiquing A for sycophancy, false positives, and missed issues. Loop cap = 3 rounds. |
14+
15+
## Marker conventions (HTML comments, used for idempotency)
16+
17+
- `<!-- wheels-bot:triage:<issue> -->` + `<!-- wheels-bot:triage-class:<bug|framework-design|other> -->` (+ optional `<!-- wheels-bot:triage-confidence:high|medium -->` — either fires propose-fix; low omitted)
18+
- `<!-- wheels-bot:research:<issue> -->` (+ optional `<!-- wheels-bot:research-confidence:high|medium -->` — either fires propose-fix; low omitted)
19+
- `<!-- wheels-bot:fix:<issue> -->` / `<!-- wheels-bot:fix-held:<issue> -->`
20+
- `<!-- wheels-bot:review-a:<pr>:<sha> -->`
21+
- `<!-- wheels-bot:review-b:<pr>:<sha>:<round> -->`
22+
- `<!-- wheels-bot:auto-close:<issue> -->`
23+
24+
## Allow-listed scopes per stage
25+
26+
Every bot-authored commit must conform to the `commitlint.config.js` allowlist (see CLAUDE.md § Commit Message Conventions). The bot's prompt (`.claude/commands/_shared-rails.md`) re-states the allowlist verbatim.
27+
28+
## Kill switch
29+
30+
Flip the repo variable `WHEELS_BOT_ENABLED` to `false` to halt every bot workflow without code changes. Add the `[skip-claude]` label (or `[skip-claude]` in the title) to halt activity on a single issue/PR.
31+
32+
## Auto-fire safety net
33+
34+
The bot is permitted to chain stages (triage → research → propose-fix), and handoff fires on `*-confidence:high` OR `*-confidence:medium`. Low stays manual. Sensitive areas (security, middleware, migrations, deploy, DI, cross-engine) are caught by the propose-fix prompt's own step-4 safety net, which posts a `fix-held` marker instead of opening a PR. Reviewer A and B then critique whatever propose-fix produces, escalating to the Senior Advisor on deadlock. All bot PRs land as `--draft` and require a human approving review on `develop`.

.claude/commands/research-frameworks.md

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -166,7 +166,13 @@ Read `.claude/commands/_shared-rails.md` first. Highlights for this command:
166166
167167
Where `<CONFIDENCE_MARKER>` is:
168168
- `<!-- wheels-bot:research-confidence:high -->` if confidence is high
169-
- omitted otherwise
169+
- `<!-- wheels-bot:research-confidence:medium -->` if confidence is medium
170+
- omitted if confidence is low
171+
172+
Both `high` and `medium` markers trigger auto-fire of
173+
`bot-propose-fix.yml`. Low stays manual — material framework
174+
disagreement or new-infrastructure proposals warrant a human discussion
175+
before any code is written.
170176
171177
7. **Self-check before posting.**
172178
- Have you cited at least one URL per framework?

.claude/commands/triage-issue.md

Lines changed: 27 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -102,15 +102,18 @@ below. Highlights for this command:
102102
existing structure under
103103
`web/sites/guides/src/content/docs/v4-0-0-snapshot/`, the work is
104104
mostly translation-of-code (not requiring deep design decisions).
105-
High-confidence docs-requests trigger the auto-fire write-docs
106-
workflow.
107105
- **`medium`**: the gap is real but the right page/path is ambiguous,
108106
OR a new top-level section is needed (a structural design decision),
109107
OR the docs require significant code investigation to write
110108
accurately.
111109
- **`low`**: the gap is vague, the scope is large (e.g. "rewrite the X
112110
chapter"), or it's not clear what concretely needs to exist.
113111

112+
**Auto-fire threshold:** both `high` and `medium` docs-requests trigger
113+
the auto-fire write-docs workflow. Write-docs has its own safety net
114+
for structural docs-architecture decisions (posts a `docs-held` marker
115+
instead of opening a PR). Only `low` stays manual.
116+
114117
Post the triage comment:
115118

116119
```
@@ -133,8 +136,11 @@ below. Highlights for this command:
133136

134137
Where `<CONFIDENCE_MARKER>` is:
135138
- `<!-- wheels-bot:docs-confidence:high -->` if confidence is high
136-
(triggers auto-fire of `bot-write-docs.yml`)
137-
- omitted otherwise (medium/low confidence does not auto-trigger)
139+
- `<!-- wheels-bot:docs-confidence:medium -->` if confidence is medium
140+
- omitted if confidence is low (low does not auto-trigger)
141+
142+
Both `high` and `medium` markers trigger auto-fire of
143+
`bot-write-docs.yml`.
138144

139145
Then exit.
140146

@@ -155,16 +161,25 @@ below. Highlights for this command:
155161

156162
- **`high`**: the report has a clear "what happened / what I expected"
157163
description; the suspected layer is unambiguous; the fix sketch is
158-
mechanical (one file, no design decisions). High-confidence bugs
159-
trigger the auto-fire fix-PR workflow.
164+
mechanical (one file, no design decisions).
160165
- **`medium`**: the report is clear but the fix has design trade-offs,
161166
OR cross-engine concerns may exist, OR more than one layer is
162167
plausibly involved.
163168
- **`low`**: the report is ambiguous; reproduction steps are vague;
164169
environment-specific symptoms are suspected; the fix shape isn't
165170
obvious from the issue body alone.
166171

167-
**Auto-downgrade rules** (force at least one level lower):
172+
**Auto-fire threshold:** both `high` and `medium` bugs trigger the
173+
auto-fire propose-fix workflow. Propose-fix has its own step-4 safety
174+
net for sensitive areas (security / middleware / migrations / deploy /
175+
DI / cross-engine) — it posts a `fix-held` marker instead of opening a
176+
PR. Reviewer A and B will critique whatever propose-fix produces. Only
177+
`low` stays manual.
178+
179+
**Auto-downgrade rules** (force at least one level lower — these
180+
downgrades are still informative for humans skimming the comment;
181+
`medium` ratings still auto-fire, but propose-fix's safety net will
182+
catch the sensitive-area cases):
168183
- The fix would touch `vendor/wheels/security/**`, auth flows, or any
169184
`vendor/wheels/middleware/**` → at most `medium`
170185
- Cross-engine concern detected (Lucee vs Adobe vs BoxLang behavior
@@ -199,7 +214,11 @@ below. Highlights for this command:
199214

200215
Where `<CONFIDENCE_MARKER>` is:
201216
- `<!-- wheels-bot:triage-confidence:high -->` if confidence is high
202-
- omitted otherwise (medium/low confidence does not auto-trigger fix-PR)
217+
- `<!-- wheels-bot:triage-confidence:medium -->` if confidence is medium
218+
- omitted if confidence is low (low does not auto-trigger fix-PR)
219+
220+
Both `high` and `medium` markers trigger auto-fire of
221+
`bot-propose-fix.yml`.
203222

204223
8. **Self-check before posting.**
205224
- Is the classification justified by quoted text from the issue?

0 commit comments

Comments
 (0)