command_tower 0.17.0 → 0.18.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/README.md +8 -8
- data/app/controllers/command_tower/me/inbox_controller.rb +16 -0
- data/app/deserializers/command_tower/deserializers/messaging/inbox.rb +85 -0
- data/app/jobs/command_tower/messaging/communications/produce_recipient_job.rb +2 -0
- data/app/models/command_tower/messaging/communication.rb +31 -0
- data/app/serializers/command_tower/serializers/messaging/inbox.rb +43 -0
- data/app/services/command_tower/messaging/accept/coordinator.rb +16 -1
- data/app/services/command_tower/messaging/accept/persister.rb +13 -0
- data/app/services/command_tower/messaging/contract/mappers/communication_mapper.rb +7 -0
- data/app/services/command_tower/messaging/contract/results/communication_result.rb +1 -0
- data/app/services/command_tower/messaging/inbox/conversation_result.rb +31 -0
- data/app/services/command_tower/messaging/inbox/entry_result.rb +36 -0
- data/app/services/command_tower/messaging/inbox/reader.rb +231 -18
- data/app/services/command_tower/messaging/inbox.rb +4 -0
- data/app/services/command_tower/messaging/rendering/inbox_document_renderer.rb +53 -36
- data/app/services/command_tower/messaging/rendering/inbox_presentation_resolver.rb +52 -0
- data/app/services/command_tower/messaging/rendering/inbox_presentation_snapshot.rb +64 -0
- data/app/services/command_tower/messaging.rb +5 -1
- data/app/services/command_tower/services/messaging/communications/produce.rb +4 -0
- data/app/services/command_tower/services/messaging/communications/produce_many.rb +6 -0
- data/app/services/command_tower/services/messaging/inbox.rb +47 -1
- data/app/workflows/command_tower/workflows/messaging/communications/produce_recipient_workflow.rb +5 -1
- data/app/workflows/command_tower/workflows/messaging/inbox.rb +15 -1
- data/config/routes.rb +1 -0
- data/db/migrate/20261010000001_add_inbox_presentation_snapshot_to_messaging_communications.rb +7 -0
- data/db/migrate/20261010000002_add_conversation_identity_to_messaging_communications.rb +10 -0
- data/docs/authorization.md +1 -1
- data/docs/bootstrap/00-ownership.md +77 -0
- data/docs/bootstrap/01-docker-make-compose.md +267 -0
- data/docs/bootstrap/02-create-the-rails-app.md +75 -0
- data/docs/bootstrap/03-pin-the-gem.md +53 -0
- data/docs/bootstrap/04-secrets-and-env.md +59 -0
- data/docs/bootstrap/05-install-migrate-doctor.md +74 -0
- data/docs/bootstrap/06-mount-and-health.md +53 -0
- data/docs/bootstrap/07-execution-bases.md +38 -0
- data/docs/bootstrap/08-initializer.md +90 -0
- data/docs/bootstrap/09-rbac.md +83 -0
- data/docs/bootstrap/10-roles-and-gates.md +62 -0
- data/docs/bootstrap/11-auth-client-path.md +57 -0
- data/docs/bootstrap/12-smoke-check.md +81 -0
- data/docs/bootstrap/13-optional.md +50 -0
- data/docs/bootstrap/14-sanity-checks.md +44 -0
- data/docs/bootstrap/README.md +90 -0
- data/docs/cookie_authentication_guide.md +11 -0
- data/docs/extending.md +3 -3
- data/docs/host_integration_guide.md +3 -281
- data/docs/initializing.md +37 -28
- data/docs/messaging_integration_guide.md +1 -1
- data/docs/principal_capabilities.md +1 -1
- data/docs/upgrades/0.10.0.md +5 -5
- data/docs/upgrades/0.11.0.md +5 -5
- data/docs/upgrades/0.18.0.md +35 -0
- data/docs/upgrades/README.md +2 -1
- data/lib/command_tower/authorization/default.yml +1 -0
- data/lib/command_tower/install/baseline.rb +2 -0
- data/lib/command_tower/version.rb +1 -1
- metadata +25 -2
|
@@ -26,12 +26,26 @@ module CommandTower
|
|
|
26
26
|
result = CommandTower::Services::Messaging::Inbox::List.call(user:, limit:, offset:, scope:)
|
|
27
27
|
return result_or_failure(result) unless result.success?
|
|
28
28
|
|
|
29
|
-
payload = result.data[:items].map { |item| CommandTower::Serializers::Messaging::Inbox::
|
|
29
|
+
payload = result.data[:items].map { |item| CommandTower::Serializers::Messaging::Inbox::EntrySerializer.serialize(item) }
|
|
30
30
|
meta = CommandTower::Serializers::Messaging::Inbox::PaginationMetaSerializer.serialize(result.data[:pagination])
|
|
31
31
|
success(payload:, meta:, http_status: :ok)
|
|
32
32
|
end
|
|
33
33
|
end
|
|
34
34
|
|
|
35
|
+
class ConversationWorkflow < BaseWorkflow
|
|
36
|
+
def call(user:, key:, scope:, around: nil, before_id: nil, after_id: nil, limit: nil)
|
|
37
|
+
result = CommandTower::Services::Messaging::Inbox::Conversation.call(
|
|
38
|
+
user:, key:, scope:, around:, before_id:, after_id:, limit:,
|
|
39
|
+
)
|
|
40
|
+
return result_or_failure(result) unless result.success?
|
|
41
|
+
|
|
42
|
+
success(
|
|
43
|
+
payload: CommandTower::Serializers::Messaging::Inbox::ConversationSerializer.serialize(result.data[:conversation]),
|
|
44
|
+
http_status: :ok,
|
|
45
|
+
)
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
|
|
35
49
|
class ShowWorkflow < BaseWorkflow
|
|
36
50
|
def call(user:, inbox_item_id:)
|
|
37
51
|
result = CommandTower::Services::Messaging::Inbox::Show.call(user:, inbox_item_id:)
|
data/config/routes.rb
CHANGED
|
@@ -64,6 +64,7 @@ CommandTower::Engine.routes.draw do
|
|
|
64
64
|
|
|
65
65
|
resources :inbox, only: [:index, :show, :destroy], controller: "inbox" do
|
|
66
66
|
collection do
|
|
67
|
+
get :conversation
|
|
67
68
|
get :unread_count, path: "unread-count"
|
|
68
69
|
post "bulk/read", action: :bulk_read
|
|
69
70
|
post "bulk/unread", action: :bulk_unread
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
class AddConversationIdentityToMessagingCommunications < ActiveRecord::Migration[7.2]
|
|
4
|
+
def change
|
|
5
|
+
add_column :messaging_communications, :conversation_key, :string, limit: 128
|
|
6
|
+
add_column :messaging_communications, :conversation_title, :string
|
|
7
|
+
add_index :messaging_communications, %i[user_id conversation_key],
|
|
8
|
+
name: "index_messaging_communications_on_user_and_conversation_key"
|
|
9
|
+
end
|
|
10
|
+
end
|
data/docs/authorization.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
Authorization establishes **permission** after authentication. Failed authorization returns `403`.
|
|
4
4
|
|
|
5
|
-
Host `rbac_groups.yml` is a **required integration step**
|
|
5
|
+
Host `rbac_groups.yml` is a **required integration step** — see [Bootstrap — RBAC](bootstrap/09-rbac.md). CommandTower ships CT-owned entity definitions. Hosts grant those names to product roles; they must not copy CT controller mappings. Without a host role that grants Me/Auth entities, authenticated calls return **403**.
|
|
6
6
|
|
|
7
7
|
## Quick usage (host provisional)
|
|
8
8
|
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
# 00 — Ownership
|
|
2
|
+
|
|
3
|
+
## Purpose
|
|
4
|
+
|
|
5
|
+
Know what CommandTower owns vs what the host owns **before** you generate a Rails app. Wrong ownership here produces forked platform code and duplicate schema.
|
|
6
|
+
|
|
7
|
+
## Prerequisites
|
|
8
|
+
|
|
9
|
+
Read [`README.md`](README.md). This step is conceptual; no files yet.
|
|
10
|
+
|
|
11
|
+
## Engine vs host
|
|
12
|
+
|
|
13
|
+
CommandTower is a **mountable Rails engine**. The host is a Rails API application that:
|
|
14
|
+
|
|
15
|
+
- Depends on the `command_tower` gem
|
|
16
|
+
- Runs install/migrate/doctor **inside Compose**
|
|
17
|
+
- Mounts the engine
|
|
18
|
+
- Configures secrets, RBAC composition, and feature gates
|
|
19
|
+
- Adds **product** routes, workflows, and schema
|
|
20
|
+
|
|
21
|
+
The engine owns:
|
|
22
|
+
|
|
23
|
+
- CommandTower-owned schema (users, sessions, JWT-related tables, Me/Auth, messaging platform tables, and the rest of the engine `db/migrate`)
|
|
24
|
+
- Platform HTTP under the mount
|
|
25
|
+
- Workflow / service / serializer framework
|
|
26
|
+
- Default entity catalog (`lib/command_tower/authorization/default.yml`)
|
|
27
|
+
- Shared FactoryBot definitions
|
|
28
|
+
|
|
29
|
+
The host owns:
|
|
30
|
+
|
|
31
|
+
- `Dockerfile`, `docker-compose.yml`, `Makefile`
|
|
32
|
+
- Product schema, product routes, product workflows
|
|
33
|
+
- `config/initializers/command_tower.rb` after generate
|
|
34
|
+
- `config/rbac_groups.yml` (grants of **CT entity names** plus host-owned entities)
|
|
35
|
+
- Secrets in Compose / env
|
|
36
|
+
- Feature-gate choices
|
|
37
|
+
- Messaging catalogs and adapter credentials (optional)
|
|
38
|
+
|
|
39
|
+
## Schema (AD-SCH-01)
|
|
40
|
+
|
|
41
|
+
CommandTower is the **sole authoring authority** for CommandTower-owned schema. Hosts must **not** manually write, edit, or copy-paste CommandTower schema migrations.
|
|
42
|
+
|
|
43
|
+
Hosts install copies via `command_tower:install` / `command_tower:install:migrations`, then `db:migrate`. Installed `*.command_tower.rb` files are an **execution context**, not a second authoring home.
|
|
44
|
+
|
|
45
|
+
Contract: [`../initializing.md`](../initializing.md).
|
|
46
|
+
|
|
47
|
+
## Workflow layer
|
|
48
|
+
|
|
49
|
+
Meaningful business behavior enters through a **workflow**. Controllers, jobs, and scheduled tasks do not call services as a full business action. Workflows do not call other workflows (extract a shared sequence). See [`../architecture.md`](../architecture.md).
|
|
50
|
+
|
|
51
|
+
Do not invent a parallel “manager” layer.
|
|
52
|
+
|
|
53
|
+
## Dummy `rails_app` is not a product template
|
|
54
|
+
|
|
55
|
+
[`../../rails_app/`](../../rails_app/) is the **in-engine dummy host** used to develop and test the gem. Later steps cite its initializer and `rbac_groups.yml` for **shape**.
|
|
56
|
+
|
|
57
|
+
Do not copy `rails_app/` as a new product. Do not copy this gem’s [`docker-compose.yaml`](../../docker-compose.yaml) (`mysql:latest`, engine ports) — that file is for **engine development**, not a product host.
|
|
58
|
+
|
|
59
|
+
This tree writes **generic** Makefile / Compose templates in [`01-docker-make-compose.md`](01-docker-make-compose.md).
|
|
60
|
+
|
|
61
|
+
## Docker-only
|
|
62
|
+
|
|
63
|
+
No Ruby or Rails on the host OS. See the README **Docker-only** section. Step 01 exists so `rails new` never runs on the Mac.
|
|
64
|
+
|
|
65
|
+
## What a platform-proof host is
|
|
66
|
+
|
|
67
|
+
Enough to:
|
|
68
|
+
|
|
69
|
+
1. Boot in Compose
|
|
70
|
+
2. Pass `make doctor`
|
|
71
|
+
3. Register, log in, and receive **200** from `GET /me` as a `member`
|
|
72
|
+
|
|
73
|
+
Product domain, Solid Queue workers, SMS/Pushover adapters, and admin RBAC bundles are **not** required to finish this manual.
|
|
74
|
+
|
|
75
|
+
## Stop
|
|
76
|
+
|
|
77
|
+
You can explain engine vs host, AD-SCH-01, and why Docker exists before `rails new`. Next: [`01-docker-make-compose.md`](01-docker-make-compose.md).
|
|
@@ -0,0 +1,267 @@
|
|
|
1
|
+
# 01 — Docker, Make, Compose
|
|
2
|
+
|
|
3
|
+
## Purpose
|
|
4
|
+
|
|
5
|
+
Author the local toolchain **before** any Rails app exists. After this step you can `make help` and `make build` on an empty repository. Ruby is only inside the image.
|
|
6
|
+
|
|
7
|
+
## Prerequisites
|
|
8
|
+
|
|
9
|
+
- Docker Desktop (or equivalent) with the Compose plugin
|
|
10
|
+
- Make
|
|
11
|
+
- Git
|
|
12
|
+
- Empty (or nearly empty) host repository
|
|
13
|
+
|
|
14
|
+
Do **not** install Ruby, Bundler, or Rails on the Mac.
|
|
15
|
+
|
|
16
|
+
## Files the host creates
|
|
17
|
+
|
|
18
|
+
| File | Role |
|
|
19
|
+
|------|------|
|
|
20
|
+
| `Dockerfile` | Ruby image: Bundler, MariaDB client, Node (asset/JS tooling if needed) |
|
|
21
|
+
| `docker-compose.yml` | `api`, MariaDB **10.11+**, Redis |
|
|
22
|
+
| `Makefile` | Operator surface: `build`, `rails-new`, `rails`, `migrate`, `doctor`, … |
|
|
23
|
+
| `.dockerignore` | Keep the build context small |
|
|
24
|
+
| `.env` (gitignored) | Local secrets later ([`04-secrets-and-env.md`](04-secrets-and-env.md)); create a stub if Compose interpolates vars |
|
|
25
|
+
|
|
26
|
+
The database in **this recipe** is MariaDB 10.11+ with `utf8mb4` / `utf8mb4_unicode_ci`. That is a host choice documented here. Do not copy the gem’s engine-dev compose (`mysql:latest`).
|
|
27
|
+
|
|
28
|
+
Redis is required for CommandTower cache/features that expect it. Optional `worker` is only if the host uses Solid Queue (add after the app exists).
|
|
29
|
+
|
|
30
|
+
## Procedure
|
|
31
|
+
|
|
32
|
+
### 1. `.dockerignore`
|
|
33
|
+
|
|
34
|
+
```
|
|
35
|
+
.git
|
|
36
|
+
log
|
|
37
|
+
tmp
|
|
38
|
+
.env
|
|
39
|
+
coverage
|
|
40
|
+
vendor/bundle
|
|
41
|
+
node_modules
|
|
42
|
+
.DS_Store
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
### 2. `Dockerfile`
|
|
46
|
+
|
|
47
|
+
Use an MRI image CommandTower’s doctor accepts (Rails **>= 7 and < 9**). A current floor that matches engine CI is Ruby **4.0.x** and Rails **8.1**. Do not freeze an older patch than the gem you will pin in step 03.
|
|
48
|
+
|
|
49
|
+
The image must **bind Puma on `0.0.0.0`**. Binding `127.0.0.1` inside the container is not reachable from the Mac.
|
|
50
|
+
|
|
51
|
+
```dockerfile
|
|
52
|
+
FROM ruby:4.0.1
|
|
53
|
+
|
|
54
|
+
ENV RAILS_LOG_TO_STDOUT=1
|
|
55
|
+
ENV BUNDLE_PATH=/usr/local/bundle
|
|
56
|
+
ENV BUNDLE_JOBS=4
|
|
57
|
+
|
|
58
|
+
RUN apt-get update -qq && apt-get install --no-install-recommends -y \
|
|
59
|
+
build-essential \
|
|
60
|
+
git \
|
|
61
|
+
default-libmysqlclient-dev \
|
|
62
|
+
default-mysql-client \
|
|
63
|
+
libyaml-dev \
|
|
64
|
+
pkg-config \
|
|
65
|
+
redis-tools \
|
|
66
|
+
&& rm -rf /var/lib/apt/lists/*
|
|
67
|
+
|
|
68
|
+
WORKDIR /app
|
|
69
|
+
|
|
70
|
+
# Gemfile does not exist until `make rails-new`. Copy the context; install gems when present.
|
|
71
|
+
COPY . .
|
|
72
|
+
RUN if [ -f Gemfile ]; then bundle install; fi
|
|
73
|
+
|
|
74
|
+
EXPOSE 3000
|
|
75
|
+
CMD ["bundle", "exec", "bin/rails", "server", "-b", "0.0.0.0", "-p", "3000"]
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
Until [`02-create-the-rails-app.md`](02-create-the-rails-app.md) runs, there is **no** `Gemfile`. That is expected. `make build` still produces an image with Ruby, Bundler, and the MariaDB client. `make rails-new` uses that image.
|
|
79
|
+
|
|
80
|
+
After `rails new` you will have a Gemfile; rebuild so `bundle install` during image build caches gems. Day-to-day `make bundle` still refreshes gems in the running volume.
|
|
81
|
+
|
|
82
|
+
### 3. `docker-compose.yml`
|
|
83
|
+
|
|
84
|
+
```yaml
|
|
85
|
+
services:
|
|
86
|
+
api:
|
|
87
|
+
build: .
|
|
88
|
+
working_dir: /app
|
|
89
|
+
command: bundle exec bin/rails server -b 0.0.0.0 -p 3000
|
|
90
|
+
volumes:
|
|
91
|
+
- .:/app
|
|
92
|
+
- bundle_cache:/usr/local/bundle
|
|
93
|
+
ports:
|
|
94
|
+
- "3000:3000"
|
|
95
|
+
environment:
|
|
96
|
+
RAILS_ENV: development
|
|
97
|
+
DATABASE_URL: mysql2://app:app@db:3306/app_development
|
|
98
|
+
REDIS_URL: redis://redis:6379/0
|
|
99
|
+
SECRET_KEY_BASE: ${SECRET_KEY_BASE:-dev-secret-key-base-change-me}
|
|
100
|
+
SIGNUP_SESSION_JWT_SECRET: ${SIGNUP_SESSION_JWT_SECRET:-dev-signup-session-change-me}
|
|
101
|
+
PASSWORD_RECOVERY_SESSION_JWT_SECRET: ${PASSWORD_RECOVERY_SESSION_JWT_SECRET:-dev-password-recovery-change-me}
|
|
102
|
+
CORS_ALLOWED_ORIGINS: ${CORS_ALLOWED_ORIGINS:-http://localhost:8081,http://localhost:8082,http://localhost:19006}
|
|
103
|
+
depends_on:
|
|
104
|
+
db:
|
|
105
|
+
condition: service_healthy
|
|
106
|
+
redis:
|
|
107
|
+
condition: service_started
|
|
108
|
+
stdin_open: true
|
|
109
|
+
tty: true
|
|
110
|
+
|
|
111
|
+
db:
|
|
112
|
+
image: mariadb:10.11
|
|
113
|
+
environment:
|
|
114
|
+
MARIADB_ROOT_PASSWORD: root
|
|
115
|
+
MARIADB_DATABASE: app_development
|
|
116
|
+
MARIADB_USER: app
|
|
117
|
+
MARIADB_PASSWORD: app
|
|
118
|
+
ports:
|
|
119
|
+
- "3306:3306"
|
|
120
|
+
volumes:
|
|
121
|
+
- mariadb_data:/var/lib/mysql
|
|
122
|
+
command:
|
|
123
|
+
- --character-set-server=utf8mb4
|
|
124
|
+
- --collation-server=utf8mb4_unicode_ci
|
|
125
|
+
healthcheck:
|
|
126
|
+
test: ["CMD", "healthcheck.sh", "--connect", "--innodb_initialized"]
|
|
127
|
+
interval: 5s
|
|
128
|
+
timeout: 5s
|
|
129
|
+
retries: 20
|
|
130
|
+
|
|
131
|
+
redis:
|
|
132
|
+
image: redis:7-alpine
|
|
133
|
+
ports:
|
|
134
|
+
- "6379:6379"
|
|
135
|
+
|
|
136
|
+
volumes:
|
|
137
|
+
bundle_cache:
|
|
138
|
+
mariadb_data:
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
Rename `app_development` / user / password to product names when you have them. Keep utf8mb4.
|
|
142
|
+
|
|
143
|
+
`command:` (and the Dockerfile `CMD`) must use `-b 0.0.0.0`. Without it, `make server` is not reachable from the Mac or from an Expo app on the host OS.
|
|
144
|
+
|
|
145
|
+
Published port **3000** is this recipe’s default for a single local API. The frontend `HostConfig` API origin must use the **same** host-published port. A second Command Tower API on this machine publishes a different host port. Engine-dev compose in this gem uses different ports; ignore it.
|
|
146
|
+
|
|
147
|
+
`CORS_ALLOWED_ORIGINS` in the sample (`8081`, `8082`, `19006`) is one web product’s origins. A second local web app uses its own Expo port. Change that origin, this API’s published port, and `config.application.url` (step 08) together. The list is consumed in [`11-auth-client-path.md`](11-auth-client-path.md). `curl` on the Mac does not need CORS.
|
|
148
|
+
|
|
149
|
+
### 4. `Makefile`
|
|
150
|
+
|
|
151
|
+
Generic operator surface. `COMPOSE` is the only Compose invocation.
|
|
152
|
+
|
|
153
|
+
```makefile
|
|
154
|
+
.PHONY: help build bundle setup fresh rebuild rails-new rails db-prepare migrate doctor \
|
|
155
|
+
bash console runner server s halt down down-v rspec test spec rubocop
|
|
156
|
+
|
|
157
|
+
COMPOSE := docker compose
|
|
158
|
+
API := $(COMPOSE) run --rm api
|
|
159
|
+
API_T := $(COMPOSE) run --rm -T api
|
|
160
|
+
|
|
161
|
+
help:
|
|
162
|
+
@echo "build Build the api image"
|
|
163
|
+
@echo "bundle bundle install in the container"
|
|
164
|
+
@echo "setup build + db-prepare"
|
|
165
|
+
@echo "fresh down-v + setup"
|
|
166
|
+
@echo "rebuild build --no-cache + bundle"
|
|
167
|
+
@echo "rails-new rails new (API) inside the container"
|
|
168
|
+
@echo "rails Forward to bin/rails (ARGS=...)"
|
|
169
|
+
@echo "db-prepare db:prepare"
|
|
170
|
+
@echo "migrate db:migrate"
|
|
171
|
+
@echo "doctor command_tower:doctor"
|
|
172
|
+
@echo "bash Shell in api"
|
|
173
|
+
@echo "console rails console"
|
|
174
|
+
@echo "runner rails runner CMD='...'"
|
|
175
|
+
@echo "server / s Foreground api server"
|
|
176
|
+
@echo "halt Stop compose"
|
|
177
|
+
@echo "down compose down"
|
|
178
|
+
@echo "down-v compose down -v (destroys DB volume)"
|
|
179
|
+
@echo "rspec/test rspec"
|
|
180
|
+
@echo "spec rspec SPEC=path"
|
|
181
|
+
@echo "rubocop rubocop"
|
|
182
|
+
|
|
183
|
+
build:
|
|
184
|
+
$(COMPOSE) build api
|
|
185
|
+
|
|
186
|
+
bundle:
|
|
187
|
+
$(API) bundle install
|
|
188
|
+
|
|
189
|
+
setup: build db-prepare
|
|
190
|
+
|
|
191
|
+
fresh: down-v setup
|
|
192
|
+
|
|
193
|
+
rebuild:
|
|
194
|
+
$(COMPOSE) build --no-cache api
|
|
195
|
+
$(MAKE) bundle
|
|
196
|
+
|
|
197
|
+
# Chicken-and-egg: image has Ruby; Gemfile may not exist yet.
|
|
198
|
+
# Doctor accepts Rails >= 7 and < 9. Prefer 8.1 to match current engine CI.
|
|
199
|
+
RAILS_NEW_VERSION ?= ~> 8.1.0
|
|
200
|
+
rails-new:
|
|
201
|
+
$(API) bash -lc 'gem install rails -v "$(RAILS_NEW_VERSION)" --no-document && rails new . --api --database=mysql --skip-git --force'
|
|
202
|
+
|
|
203
|
+
rails:
|
|
204
|
+
$(COMPOSE) run --rm -e SKIP_MOUNT -e SKIP_CONFIGURE -e FORCE api bundle exec bin/rails $(ARGS)
|
|
205
|
+
|
|
206
|
+
db-prepare:
|
|
207
|
+
$(API) bundle exec bin/rails db:prepare
|
|
208
|
+
|
|
209
|
+
migrate:
|
|
210
|
+
$(API) bundle exec bin/rails db:migrate
|
|
211
|
+
|
|
212
|
+
doctor:
|
|
213
|
+
$(API) bundle exec bin/rails command_tower:doctor
|
|
214
|
+
|
|
215
|
+
bash:
|
|
216
|
+
$(COMPOSE) run --rm api bash
|
|
217
|
+
|
|
218
|
+
console:
|
|
219
|
+
$(API) bundle exec bin/rails console
|
|
220
|
+
|
|
221
|
+
runner:
|
|
222
|
+
$(API) bundle exec bin/rails runner "$(CMD)"
|
|
223
|
+
|
|
224
|
+
server s:
|
|
225
|
+
$(COMPOSE) up api
|
|
226
|
+
|
|
227
|
+
halt:
|
|
228
|
+
$(COMPOSE) stop
|
|
229
|
+
|
|
230
|
+
down:
|
|
231
|
+
$(COMPOSE) down
|
|
232
|
+
|
|
233
|
+
down-v:
|
|
234
|
+
$(COMPOSE) down -v
|
|
235
|
+
|
|
236
|
+
rspec test:
|
|
237
|
+
$(API_T) bundle exec rspec
|
|
238
|
+
|
|
239
|
+
spec:
|
|
240
|
+
$(API_T) bundle exec rspec $(SPEC)
|
|
241
|
+
|
|
242
|
+
rubocop:
|
|
243
|
+
$(API_T) bundle exec rubocop
|
|
244
|
+
```
|
|
245
|
+
|
|
246
|
+
`rails-new` installs the Rails **gem inside the ephemeral container** and writes the app onto the mounted `/app`. That is still Docker-only: the Mac never runs `gem` or `rails`.
|
|
247
|
+
|
|
248
|
+
After the app exists, prefer `make rails ARGS='…'` over re-running `rails-new`.
|
|
249
|
+
|
|
250
|
+
Flags such as `SKIP_MOUNT=1` belong on the Make line. The `rails` recipe forwards `SKIP_MOUNT`, `SKIP_CONFIGURE`, and `FORCE` into the container:
|
|
251
|
+
|
|
252
|
+
```bash
|
|
253
|
+
SKIP_MOUNT=1 make rails ARGS='command_tower:install'
|
|
254
|
+
```
|
|
255
|
+
|
|
256
|
+
### 5. Build
|
|
257
|
+
|
|
258
|
+
```bash
|
|
259
|
+
make help
|
|
260
|
+
make build
|
|
261
|
+
```
|
|
262
|
+
|
|
263
|
+
`make build` may warn that `Gemfile` is missing. That is expected until step 02.
|
|
264
|
+
|
|
265
|
+
## Stop
|
|
266
|
+
|
|
267
|
+
`make help` prints the operator surface. `make build` produces an `api` image with Ruby. There is still no Rails application. Next: [`02-create-the-rails-app.md`](02-create-the-rails-app.md).
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
# 02 — Create the Rails app
|
|
2
|
+
|
|
3
|
+
## Purpose
|
|
4
|
+
|
|
5
|
+
Generate a Rails **8.1 API** application **inside the Compose image**. The host OS never receives a `rails` binary.
|
|
6
|
+
|
|
7
|
+
## Prerequisites
|
|
8
|
+
|
|
9
|
+
- [`01-docker-make-compose.md`](01-docker-make-compose.md) complete: `make build` works
|
|
10
|
+
- Working directory is the host repository (already contains Dockerfile / Compose / Makefile)
|
|
11
|
+
|
|
12
|
+
## Files this step creates (inside the container, onto the mount)
|
|
13
|
+
|
|
14
|
+
Rails’ `rails new .` writes `Gemfile`, `config/`, `app/`, `bin/`, and the rest of a standard API app into the current directory.
|
|
15
|
+
|
|
16
|
+
`--force` is required because Dockerfile, Compose, and Makefile already exist.
|
|
17
|
+
|
|
18
|
+
## Procedure
|
|
19
|
+
|
|
20
|
+
```bash
|
|
21
|
+
make rails-new
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
That target (from step 01) runs in the `api` service:
|
|
25
|
+
|
|
26
|
+
```text
|
|
27
|
+
gem install rails -v "~> 8.1.0" --no-document
|
|
28
|
+
rails new . --api --database=mysql --skip-git --force
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
Both commands run **in the container**. `--skip-git` avoids nesting a repo; you already have Git on the host. `--database=mysql` matches MariaDB via the `mysql2` adapter.
|
|
32
|
+
|
|
33
|
+
If `rails new` overwrites `Dockerfile`, `docker-compose.yml`, or `Makefile`, **restore the files from step 01**. Rails generators are not the source of the host toolchain. Confirm `command:` / `CMD` still bind `-b 0.0.0.0`.
|
|
34
|
+
|
|
35
|
+
Then add host gems CommandTower does not replace. In `Gemfile` (keep `mysql2` and `puma` from `rails new`):
|
|
36
|
+
|
|
37
|
+
```ruby
|
|
38
|
+
gem "rack-cors"
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
`command_tower` (step 03) already depends on `redis`. You do not need a second Redis gem unless the host uses Redis for its own cache.
|
|
42
|
+
|
|
43
|
+
Set `config/database.yml` so development points at the `db` service, not `localhost` on the Mac:
|
|
44
|
+
|
|
45
|
+
```yaml
|
|
46
|
+
development:
|
|
47
|
+
adapter: mysql2
|
|
48
|
+
encoding: utf8mb4
|
|
49
|
+
collation: utf8mb4_unicode_ci
|
|
50
|
+
url: <%= ENV.fetch("DATABASE_URL") %>
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
Leave test until you add a test database; `db:prepare` can create it later.
|
|
54
|
+
|
|
55
|
+
Local mail for verify/reset codes (Compose-mounted `tmp/` is visible on the Mac):
|
|
56
|
+
|
|
57
|
+
```ruby
|
|
58
|
+
# config/environments/development.rb
|
|
59
|
+
config.action_mailer.perform_deliveries = true
|
|
60
|
+
config.action_mailer.delivery_method = :file
|
|
61
|
+
config.action_mailer.file_settings = { location: Rails.root.join("tmp/mails") }
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
Then:
|
|
65
|
+
|
|
66
|
+
```bash
|
|
67
|
+
make build
|
|
68
|
+
make bundle
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
Do not run `rails new` on the Mac. Do not `brew install ruby` to make this step “easier.”
|
|
72
|
+
|
|
73
|
+
## Stop
|
|
74
|
+
|
|
75
|
+
`Gemfile` exists, `bin/rails` exists **in the repo** (executed only via `make rails`). Dockerfile/Compose from step 01 are still the operator toolchain. `make bundle` succeeds. Next: [`03-pin-the-gem.md`](03-pin-the-gem.md).
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
# 03 — Pin the CommandTower gem
|
|
2
|
+
|
|
3
|
+
## Purpose
|
|
4
|
+
|
|
5
|
+
Depend on released CommandTower from **RubyGems** (or a git tag). Path pins are maintainer Local mode, not the committed default for a new host.
|
|
6
|
+
|
|
7
|
+
## Prerequisites
|
|
8
|
+
|
|
9
|
+
- [`02-create-the-rails-app.md`](02-create-the-rails-app.md): `make bundle` works
|
|
10
|
+
- You know the CommandTower version (or git tag) this host should adopt
|
|
11
|
+
|
|
12
|
+
## Files the host edits
|
|
13
|
+
|
|
14
|
+
`Gemfile` — add:
|
|
15
|
+
|
|
16
|
+
```ruby
|
|
17
|
+
gem "command_tower"
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
Pin when you need a floor:
|
|
21
|
+
|
|
22
|
+
```ruby
|
|
23
|
+
gem "command_tower", "~> X.Y"
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
Git tag (only if your org does not publish the gem to the RubyGems source the host uses):
|
|
27
|
+
|
|
28
|
+
```ruby
|
|
29
|
+
gem "command_tower", git: "https://github.com/<org>/command_tower.git", tag: "vX.Y.Z"
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
**Path pins** (`path: "../command_tower"`) are for engine maintainers iterating locally. They are not the default a new product host commits. If you use a path pin in a workspace, do not document it as the production pin.
|
|
33
|
+
|
|
34
|
+
## Procedure
|
|
35
|
+
|
|
36
|
+
1. Add the gem line.
|
|
37
|
+
2. Refresh lockfile **in the container**:
|
|
38
|
+
|
|
39
|
+
```bash
|
|
40
|
+
make bundle
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
3. Rebuild if the image copies `Gemfile` / `Gemfile.lock` at build time:
|
|
44
|
+
|
|
45
|
+
```bash
|
|
46
|
+
make build
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
Do not run `bundle install` on the Mac.
|
|
50
|
+
|
|
51
|
+
## Stop
|
|
52
|
+
|
|
53
|
+
`Gemfile.lock` lists `command_tower`. `make bundle` succeeds. Next: [`04-secrets-and-env.md`](04-secrets-and-env.md).
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
# 04 — Secrets and env
|
|
2
|
+
|
|
3
|
+
## Purpose
|
|
4
|
+
|
|
5
|
+
Give Compose the secrets CommandTower **doctor** requires. Without them, install may succeed and doctor will fail.
|
|
6
|
+
|
|
7
|
+
## Prerequisites
|
|
8
|
+
|
|
9
|
+
- [`03-pin-the-gem.md`](03-pin-the-gem.md)
|
|
10
|
+
- Compose file from [`01-docker-make-compose.md`](01-docker-make-compose.md)
|
|
11
|
+
|
|
12
|
+
## Files the host creates / edits
|
|
13
|
+
|
|
14
|
+
| File | Role |
|
|
15
|
+
|------|------|
|
|
16
|
+
| `.env` (gitignored) | Local values interpolated by Compose |
|
|
17
|
+
| `.env.example` (committed, no secrets) | Names only |
|
|
18
|
+
| `docker-compose.yml` `environment:` | Already stubbed in step 01 |
|
|
19
|
+
|
|
20
|
+
Required (names match [`../initializing.md`](../initializing.md)):
|
|
21
|
+
|
|
22
|
+
| Secret | Typical env | Used for |
|
|
23
|
+
|--------|-------------|----------|
|
|
24
|
+
| JWT HMAC | `SECRET_KEY_BASE` (or `config.jwt.hmac_secret`) | Access tokens |
|
|
25
|
+
| Signup-session JWT | `SIGNUP_SESSION_JWT_SECRET` | Email verification / signup session |
|
|
26
|
+
| Password-recovery JWT | `PASSWORD_RECOVERY_SESSION_JWT_SECRET` | Password reset session |
|
|
27
|
+
|
|
28
|
+
Doctor checks these. Failures abort with remediation text.
|
|
29
|
+
|
|
30
|
+
`.env.example`:
|
|
31
|
+
|
|
32
|
+
```
|
|
33
|
+
SECRET_KEY_BASE=
|
|
34
|
+
SIGNUP_SESSION_JWT_SECRET=
|
|
35
|
+
PASSWORD_RECOVERY_SESSION_JWT_SECRET=
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
Generate long random values for `.env`. Do not commit `.env`. Do not reuse production secrets in development.
|
|
39
|
+
|
|
40
|
+
The initializer generated in step 05 can read these via `ENV.fetch`. You do not need a complete initializer yet.
|
|
41
|
+
|
|
42
|
+
Optional later: SMTP, SMS, Pushover credentials — [`13-optional.md`](13-optional.md) and [`../messaging_integration_guide.md`](../messaging_integration_guide.md). Not required for `GET /me`.
|
|
43
|
+
|
|
44
|
+
This step is **local** env for Compose. Production secret distribution is out of scope ([README](README.md#this-is-not-deployment)).
|
|
45
|
+
|
|
46
|
+
## Procedure
|
|
47
|
+
|
|
48
|
+
1. Create `.env` with three distinct secrets.
|
|
49
|
+
2. Confirm `docker-compose.yml` passes them into `api`.
|
|
50
|
+
3. Recreate the api container if it was already running:
|
|
51
|
+
|
|
52
|
+
```bash
|
|
53
|
+
make halt
|
|
54
|
+
make build
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
## Stop
|
|
58
|
+
|
|
59
|
+
Compose interpolates the three secrets into `api`. Next: [`05-install-migrate-doctor.md`](05-install-migrate-doctor.md).
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
# 05 — Install, migrate, doctor
|
|
2
|
+
|
|
3
|
+
## Purpose
|
|
4
|
+
|
|
5
|
+
Copy engine migrations, generate the initializer (and default mount unless skipped), migrate, and run doctor. **Invocation is Make.** The contract for flags and schema is [`../initializing.md`](../initializing.md) — do not treat this page as a second copy of that contract.
|
|
6
|
+
|
|
7
|
+
## Prerequisites
|
|
8
|
+
|
|
9
|
+
- [`04-secrets-and-env.md`](04-secrets-and-env.md)
|
|
10
|
+
- Database service healthy (`make` recipes that run Rails will start `depends_on`)
|
|
11
|
+
|
|
12
|
+
## Files this step creates
|
|
13
|
+
|
|
14
|
+
| File | Role |
|
|
15
|
+
|------|------|
|
|
16
|
+
| `db/migrate/*command_tower.rb` | Installed copies of engine migrations (do not hand-edit) |
|
|
17
|
+
| `config/initializers/command_tower.rb` | Generated unless `SKIP_CONFIGURE=1` |
|
|
18
|
+
| Engine mount in `config/routes.rb` | Unless `SKIP_MOUNT=1` or `--skip-routes` |
|
|
19
|
+
|
|
20
|
+
`command_tower:install` does **not** run `db:migrate`. Installing and migrating stay separate on purpose.
|
|
21
|
+
|
|
22
|
+
## Procedure
|
|
23
|
+
|
|
24
|
+
If the host will own `GET /api/healthz` (or any route that must be declared **before** the engine mount), skip the generator mount and add routes in [`06-mount-and-health.md`](06-mount-and-health.md):
|
|
25
|
+
|
|
26
|
+
```bash
|
|
27
|
+
make rails ARGS='command_tower:install' SKIP_MOUNT=1
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
Otherwise:
|
|
31
|
+
|
|
32
|
+
```bash
|
|
33
|
+
make rails ARGS='command_tower:install'
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
Then:
|
|
37
|
+
|
|
38
|
+
```bash
|
|
39
|
+
make db-prepare
|
|
40
|
+
make migrate
|
|
41
|
+
make doctor
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
`db-prepare` creates the development database if needed. `migrate` applies host and CommandTower migrations.
|
|
45
|
+
|
|
46
|
+
Existing customized hosts (initializer and mount already present):
|
|
47
|
+
|
|
48
|
+
```bash
|
|
49
|
+
make rails ARGS='command_tower:install' SKIP_CONFIGURE=1
|
|
50
|
+
make migrate
|
|
51
|
+
make doctor
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
`FORCE=1` overwrites `config/initializers/command_tower.rb` — do not use it after you have customized the initializer unless you intend to regenerate.
|
|
55
|
+
|
|
56
|
+
The **task name** inside the container is `bin/rails command_tower:install`. Operators never type that on the Mac.
|
|
57
|
+
|
|
58
|
+
### What install does (summary)
|
|
59
|
+
|
|
60
|
+
1. `command_tower:install:migrations` — copies engine migrations with `*.command_tower.rb` scope.
|
|
61
|
+
2. `rails g command_tower:configure` unless skipped — initializer + mount.
|
|
62
|
+
3. Prints next steps.
|
|
63
|
+
|
|
64
|
+
Full flag table: [`../initializing.md`](../initializing.md).
|
|
65
|
+
|
|
66
|
+
Doctor checks Rails compatibility, engine baseline migrations, host-installed copies, JWT / session secrets, and messaging adapter names. It does **not** probe Redis/SMTP connectivity.
|
|
67
|
+
|
|
68
|
+
## In-engine dummy
|
|
69
|
+
|
|
70
|
+
Generated initializer shape: [`../../rails_app/config/initializers/command_tower.rb`](../../rails_app/config/initializers/command_tower.rb). That file is a dummy-host example, not a product dump.
|
|
71
|
+
|
|
72
|
+
## Stop
|
|
73
|
+
|
|
74
|
+
Install is idempotent if re-run. Migrations applied. `make doctor` exits 0 (or you have a documented doctor failure you will fix in steps 08–10). Next: [`06-mount-and-health.md`](06-mount-and-health.md).
|