Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
54 commits
Select commit Hold shift + click to select a range
e554fe0
docs(config): add wheels deploy kamal port design spec
bpamiri Apr 21, 2026
1a90e07
docs(config): add wheels deploy kamal port implementation plan
bpamiri Apr 21, 2026
d3bc96f
docs(config): scaffold deploy lib directory with JAR manifest
bpamiri Apr 21, 2026
56b8ea7
test(cli): add test-cli-local.sh + amend deploy plan for CLI test paths
bpamiri Apr 21, 2026
c4e0ebe
feat(cli): add Mustache template engine for deploy artifacts
bpamiri Apr 21, 2026
3872ef6
fix(cli): correct test-cli-local.sh URL + handle negative totalError
bpamiri Apr 21, 2026
30a2244
fix(cli): close stale URLClassLoader + guard Mustache compiler lookup
bpamiri Apr 21, 2026
6fcf684
feat(cli): add SafeConstructor YAML parser for deploy configs
bpamiri Apr 21, 2026
4d6d8e1
feat(cli): vendor sshj 0.39.0 + BouncyCastle transitives for deploy SSH
bpamiri Apr 21, 2026
eae8567
test(cli): add dockerized sshd fixture for deploy tests
bpamiri Apr 21, 2026
6295104
feat(cli): add SshClient facade with run/upload/download via sshj
bpamiri Apr 21, 2026
b47e7a4
test(cli): add SFTP upload/download round-trip coverage for SshClient
bpamiri Apr 21, 2026
15e706e
test(cli): add FakeSshPool recording test double for Cli-layer specs
bpamiri Apr 21, 2026
29d22dc
feat(cli): add SshPool with parallel onEach/onAny/sequential fan-out
bpamiri Apr 21, 2026
d633f66
feat(cli): add host-prefixed Output sink for deploy log streaming
bpamiri Apr 21, 2026
9344da3
feat(config): add deploy config tree and ConfigLoader with overlay su…
bpamiri Apr 21, 2026
0a9f018
test(config): add ConfigLoader error-path coverage for Validator rules
bpamiri Apr 21, 2026
8acc509
feat(cli): add Commands/Base + DockerCommands string-building helpers
bpamiri Apr 21, 2026
e8d4f99
feat(cli): add AppCommands with container lifecycle strings
bpamiri Apr 21, 2026
c2a4f9a
feat(cli): add RegistryCommands for docker login/logout
bpamiri Apr 21, 2026
4b6f8c8
feat(cli): add BuilderCommands for docker build/push/pull/tag
bpamiri Apr 21, 2026
a7538ee
feat(cli): add AuditorCommands for deploy audit log append
bpamiri Apr 21, 2026
09a58e7
feat(cli): add ProxyCommands with kamal-proxy hand-off
bpamiri Apr 21, 2026
76e8f19
feat(cli): add DeployMainCli with dry-run deploy/rollback/config
bpamiri Apr 21, 2026
2c9425c
feat(cli): wire wheels deploy subcommand into Module.cfc
bpamiri Apr 21, 2026
4d1a35a
test(cli): add Phase 1 dry-run comparison scaffolding + retrospective
bpamiri Apr 21, 2026
1b03c49
fix(cli): scaffold temp project for ModuleDeploySpec so BaseModule re…
bpamiri Apr 21, 2026
b19097d
test(cli): drop ModuleDeploySpec
bpamiri Apr 21, 2026
ce21cb3
feat(cli): add LockCommands for deploy lock acquire/release/status
bpamiri Apr 21, 2026
42e89b3
feat(cli): add HookCommands with KAMAL_* env contract for dev-machine…
bpamiri Apr 21, 2026
23b8876
feat(cli): add SecretResolver for .kamal/secrets with $(cmd) expansion
bpamiri Apr 21, 2026
babb73f
feat(cli): integrate lock acquire/release and hooks into DeployMainCl…
bpamiri Apr 21, 2026
d1e659e
feat(cli): add DeployAppCli subcommand surface (boot/start/stop/logs/…
bpamiri Apr 21, 2026
9e6c609
feat(cli): add DeployProxyCli subcommand (boot/reboot/start/stop/deta…
bpamiri Apr 21, 2026
d1df1f0
feat(cli): add DeployRegistryCli subcommand (setup/login/logout/remove)
bpamiri Apr 21, 2026
65c4156
feat(cli): implement wheels deploy init with Mustache templates for d…
bpamiri Apr 21, 2026
c462115
test(cli): add E2E integration spec scaffolding (gated on DEPLOY_E2E=1)
bpamiri Apr 21, 2026
3cc4e0c
feat(cli): add accessory subcommand (sidecar container lifecycle)
bpamiri Apr 21, 2026
f1dc4ae
feat(cli): add build subcommand (deliver/push/pull/create/remove/deta…
bpamiri Apr 21, 2026
3459c77
feat(cli): add server subcommand (exec/bootstrap)
bpamiri Apr 21, 2026
1a98920
feat(cli): add prune subcommand (all/images/containers with --keep)
bpamiri Apr 21, 2026
4496041
feat(cli): add user-facing lock subcommand (acquire/release/status)
bpamiri Apr 21, 2026
0d1c1f5
feat(cli): add secrets subcommand with 5 provider adapters
bpamiri Apr 21, 2026
0b9884d
feat(cli): add audit/docs/details/remove top-level deploy verbs
bpamiri Apr 21, 2026
5773029
test(cli): expand deploy verb-coverage smoke + config-diff harness
bpamiri Apr 21, 2026
80ddf81
ci(cli): add deploy-ci workflow gating PRs that touch the deploy subs…
bpamiri Apr 21, 2026
d9fc4ba
docs(config): add user-facing deployment documentation for wheels deploy
bpamiri Apr 21, 2026
d0e86c6
docs(config): add Deploy Quick Reference to CLAUDE.md and surface whe…
bpamiri Apr 21, 2026
ecc250f
Revert "docs(config): add user-facing deployment documentation for wh…
bpamiri Apr 21, 2026
c8675a7
feat(cli): add secrets subcommand with 5 provider adapters
bpamiri Apr 21, 2026
7fa86a7
fix(cli): install sqlite jdbc driver in test-cli-local.sh for fresh envs
bpamiri Apr 21, 2026
b6cf6b4
fix(cli): pre-warm lucli express dir + guard find for fresh CI runners
bpamiri Apr 21, 2026
21ff310
fix(cli): install sqlite jdbc after first server start, not before
bpamiri Apr 21, 2026
7708f65
fix(cli): scope test-cli-local.sh gating to deploy specs
bpamiri Apr 21, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
110 changes: 110 additions & 0 deletions .github/workflows/deploy-ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
# Deploy subsystem CI gate
#
# Runs the CLI test suite plus the deploy-specific harness scripts on any
# PR/push that touches the wheels-deploy subsystem (cli/lucli/services/deploy/**,
# cli/lucli/lib/deploy/**, fixtures, helpers, or the tooling itself).
#
# This is narrower than the broader `pr.yml` fast-test workflow, which runs
# the core framework test suite on every PR against `develop`. Keeping this
# one path-scoped means framework-only edits don't re-run the (slower) deploy
# matrix, and deploy-subsystem edits pick up an extra gate.
#
# Prerequisites (already present in this repo's CI):
# - LuCLI install recipe mirrors .github/workflows/pr.yml (cybersonic/LuCLI release).
# - Java 21 via actions/setup-java@v4.
#
# Gating model:
# 1. cli-tests - runs `tools/test-cli-local.sh`, must be 365+/0/0.
# 2. deploy-config-diff - informational (`continue-on-error: true`).
# The script expects Ruby Kamal on PATH *and* a routable `wheels deploy`.
# Neither is wired in CI yet, so this step's exit 1 is expected and
# non-gating until the `wheels` binary routes to this worktree's Module.cfc.
# 3. deploy-verb-smoke - calls `tools/deploy-verb-smoke.sh`, which already
# exits 0 with a warning when `wheels deploy` is unreachable. So this step
# is a no-op today, and becomes a real gate automatically the moment the
# LuCLI/Module.cfc routing lands.
#
# See `docs/superpowers/plans/2026-04-21-phase1-retrospective.md` for the
# full deploy-gap write-up.

name: Deploy Subsystem CI

on:
push:
branches: [main, develop]
paths:
- 'cli/lucli/services/deploy/**'
- 'cli/lucli/lib/deploy/**'
- 'cli/lucli/templates/deploy/**'
- 'cli/lucli/tests/_fixtures/deploy/**'
- 'cli/lucli/tests/specs/deploy/**'
- 'cli/lucli/tests/_helpers/DeployShellHelper.cfc'
- 'cli/lucli/Module.cfc'
- 'tools/deploy-*.sh'
- 'tools/deploy-dry-run-normalize.py'
- 'tools/test-cli-local.sh'
- '.github/workflows/deploy-ci.yml'
pull_request:
paths:
- 'cli/lucli/services/deploy/**'
- 'cli/lucli/lib/deploy/**'
- 'cli/lucli/templates/deploy/**'
- 'cli/lucli/tests/_fixtures/deploy/**'
- 'cli/lucli/tests/specs/deploy/**'
- 'cli/lucli/tests/_helpers/DeployShellHelper.cfc'
- 'cli/lucli/Module.cfc'
- 'tools/deploy-*.sh'
- 'tools/deploy-dry-run-normalize.py'
- 'tools/test-cli-local.sh'
- '.github/workflows/deploy-ci.yml'

permissions:
contents: read

jobs:
deploy-gate:
name: "Deploy CLI tests + verb smoke"
runs-on: ubuntu-latest
env:
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true
LUCLI_VERSION: "0.3.3"
WHEELS_CI: "true"
PORT: "8080"
steps:
- uses: actions/checkout@v5

- name: Set up JDK 21
uses: actions/setup-java@v4
with:
distribution: 'temurin'
java-version: '21'

- name: Install LuCLI
run: |
curl -sL "https://github.com/cybersonic/LuCLI/releases/download/v${LUCLI_VERSION}/lucli-${LUCLI_VERSION}-linux" \
-o /usr/local/bin/lucli
chmod +x /usr/local/bin/lucli
lucli --version

- name: Create test databases
run: |
sudo apt-get update -y && sudo apt-get install -y --no-install-recommends sqlite3
sqlite3 wheelstestdb.db "SELECT 1;"
sqlite3 wheelstestdb_tenant_b.db "SELECT 1;"

- name: Run CLI test suite
run: bash tools/test-cli-local.sh

- name: Run deploy-config-diff (informational)
continue-on-error: true
run: |
bash tools/deploy-config-diff.sh \
|| echo "deploy-config-diff: informational, non-gating (expected until 'wheels deploy' routes in CI)"

- name: Run deploy-verb-smoke (gates when wheels deploy reachable)
run: bash tools/deploy-verb-smoke.sh

- name: Debug server logs
if: failure()
run: |
cat /tmp/wheels-cli-test-server.log 2>/dev/null || true
84 changes: 83 additions & 1 deletion CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -739,6 +739,88 @@ wheels jobs monitor # live dashboard

The `wheels_jobs` table is auto-created by `Job.cfc::$ensureJobTable()` on first enqueue or processing — no migration needed. (The older `20260221000001_createwheels_jobs_table.cfc` migration is vestigial; Phase 2b drift audit confirmed auto-create is now the path.)

## Deploy Quick Reference

`wheels deploy` ships your 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.

wheels deploy init # scaffold config/deploy.yml + .kamal/secrets
wheels deploy setup # one-time server bootstrap + first deploy
wheels deploy # rolling deploy
wheels deploy --dry-run # print commands without executing
wheels deploy rollback v1 # roll back to a previous version
wheels deploy config # print resolved config as YAML
wheels deploy version # show Kamal version this port mirrors

### Subcommands

```cfm
wheels deploy app <verb> // boot/start/stop/details/containers/images/logs/live/maintenance/remove
wheels deploy proxy <verb> // boot/reboot/start/stop/restart/details/logs/remove
wheels deploy accessory <verb> // boot/reboot/start/stop/restart/details/logs/remove (sidecars: db/redis/search)
wheels deploy build <verb> // deliver/push/pull/create/remove/details/dev
wheels deploy registry <verb> // setup/login/logout/remove
wheels deploy server <verb> // exec/bootstrap
wheels deploy prune <verb> // all/images/containers [--keep=N]
wheels deploy lock <verb> // acquire/release/status (manual — normal deploys auto-lock)
wheels deploy secrets <verb> // fetch/extract/print (adapters: op/bitwarden/aws/lastpass/doppler)
wheels deploy audit // tail /tmp/kamal-audit.log on each server
wheels deploy details // aggregate app + proxy + accessory status
wheels deploy remove --confirm // teardown all app/proxy/accessory containers
wheels deploy docs [section] // in-terminal config reference
```

### On-server parity contract (byte-compatible with Ruby Kamal)

- Container names: `<service>-<role>-<version>`
- Labels: `service=`, `role=`, `destination=`, `version=`
- Docker network: `kamal`
- Lock file: `/tmp/kamal_deploy_lock_<service>`
- Proxy config: `/home/<user>/.config/kamal-proxy/`
- Hook env prefix: `KAMAL_*` (never `WHEELS_*` — user hooks migrate unchanged)

A server managed by Ruby Kamal can be taken over by `wheels deploy` without cleanup.

### Architecture

```
cli/lucli/services/deploy/
├── cli/*.cfc DeployMainCli + Deploy<App|Proxy|Accessory|Build|Registry|Server|Prune|Lock|Secrets>Cli
├── commands/*.cfc Base + Docker/App/Proxy/Builder/Registry/Auditor/Lock/Hook/Accessory/PruneCommands
├── config/*.cfc Config + Role/Env/Builder/Proxy/Registry/Ssh/Accessory/Validator/ConfigLoader
├── lib/*.cfc JarLoader/Mustache/Yaml/SshClient/SshPool/FakeSshPool/Output/SecretResolver
└── secrets/*.cfc BaseAdapter + OnePassword/Bitwarden/AwsSecrets/LastPass/Doppler adapters

cli/lucli/lib/deploy/*.jar jmustache, snakeyaml, sshj + BouncyCastle transitives (URLClassLoader-isolated)
cli/lucli/templates/deploy/ Mustache templates for `wheels deploy init` output
```

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.

### Critical gotchas

1. **Kamal-compatible schema, ONE divergence.** ERB in `deploy.yml` is NOT supported. Use the restricted Mustache context `{{env.FOO}}`, `{{destination}}`, `{{hostname}}` instead. Everything else in `config/deploy.yml` is byte-identical to Kamal 2.4.0.
2. **Hook env prefix is `KAMAL_`, not `WHEELS_`.** This is deliberate — it means Ruby Kamal users' existing `.kamal/hooks/` scripts work unchanged.
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.
4. **`wheels deploy remove` is destructive and requires `--confirm`.** Bare `wheels deploy remove` throws without touching anything.
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.
6. **No `--dry-run` flag in Ruby Kamal 2.8.2.** 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.

### Testing

`cli/lucli/tests/specs/deploy/` extends `wheels.wheelstest.system.BaseSpec`. Run with:

bash tools/test-cli-local.sh

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`).

### Reference docs

- User guides: `docs/src/working-with-wheels/deployment/` (first-deploy, config-reference, accessories, secrets, hooks, migrating-from-kamal)
- Per-verb CLI reference: `docs/src/command-line-tools/commands/deploy/`
- Design spec: `docs/superpowers/specs/2026-04-20-wheels-deploy-kamal-port-design.md`
- Implementation plan: `docs/superpowers/plans/2026-04-20-wheels-deploy-kamal-port.md`
- Retrospective: `docs/superpowers/plans/2026-04-21-phase1-retrospective.md`

## Server-Sent Events (SSE) Quick Reference

```cfm
Expand Down Expand Up @@ -875,7 +957,7 @@ The project name is **Wheels** (not "CFWheels"). The rebrand happened at v3.0. A

Or run `wheels mcp setup` to generate `.mcp.json` + `.opencode.json` automatically.

Tools are auto-discovered from `cli/lucli/Module.cfc` public functions, prefixed with the module name (`wheels_generate`, `wheels_migrate`, `wheels_test`, `wheels_reload`, `wheels_seed`, `wheels_analyze`, `wheels_validate`, `wheels_routes`, `wheels_info`, `wheels_destroy`, `wheels_doctor`, `wheels_stats`, `wheels_notes`, `wheels_db`, `wheels_upgrade`, `wheels_create`). CLI-only tools (`mcp`, `d`, `new`, `console`, `start`, `stop`, `browser`) are hidden from MCP `tools/list` via `mcpHiddenTools()`.
Tools are auto-discovered from `cli/lucli/Module.cfc` public functions, prefixed with the module name (`wheels_generate`, `wheels_migrate`, `wheels_test`, `wheels_reload`, `wheels_seed`, `wheels_analyze`, `wheels_validate`, `wheels_routes`, `wheels_info`, `wheels_destroy`, `wheels_doctor`, `wheels_stats`, `wheels_notes`, `wheels_db`, `wheels_upgrade`, `wheels_create`, `wheels_deploy`). CLI-only tools (`mcp`, `d`, `new`, `console`, `start`, `stop`, `browser`) are hidden from MCP `tools/list` via `mcpHiddenTools()`.

Workflow orchestration (multi-step planning, feature development) is not a framework concern — use your preferred Claude Code plugin (Superpowers, feature-dev, etc.). The framework ships deterministic Wheels operations via MCP; the model orchestrates.

Expand Down
Loading
Loading