yamine 0.15.1 → 0.16.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: cb9ab4ec4d7e32278f307f3ebb9366b2a23ef7ec5b65016e6888c240ee90d951
4
- data.tar.gz: 13f8a0d5340f03de8132101f5eb996b533aaf9b60a96b21bfbdddc6839ae4641
3
+ metadata.gz: 66abd82532420b13b18597a4d71b51485f06b09c14d5b0d4c3e777d8056e7145
4
+ data.tar.gz: be3bcb4b88ba0eeb444ed33d10c14c217cf145073f2f8cce78e62e4dc138bffd
5
5
  SHA512:
6
- metadata.gz: 10b5b826f5cd2c007e181feaf9f671e0b654a80b13f922c3251d72912987ecd294fa3cdc1d17fa7e5f58dbed22aab8961598844b711c896301e4cb7b46a59399
7
- data.tar.gz: cb4962d0c9db1711963bbee31b59f3ca062613562714dfeebed81cdff5c896caf0fb3cdc16296e5245db0885da8debf03b6a1ec9b527e98bb79c3b5e4e46ff08
6
+ metadata.gz: e03ba4c4510f03424190fdf58f0c5f6db6183425b75c31628df99724bf592edc068ab13cd6bf7abd28b04c577025b4ec913eb87d892b78524aab7aee35e83dbd
7
+ data.tar.gz: 9eaa287f5ee45e231cd82f29c8dbf0bf0f3e197cc825dfeccccb4f39ff02c826167d32516c23fef7c2a39804d662a69b417e082b1b9d2e6f1b562ce63597a95e
data/CHANGELOG.md CHANGED
@@ -1,5 +1,90 @@
1
1
  # Changelog
2
2
 
3
+ ## [Unreleased]
4
+
5
+ ## [0.16.0] — 2026-09-23
6
+
7
+ ### Added
8
+
9
+ - **Multi-database worktrees — the whole set, asked not guessed.**
10
+ `worktree add` now probes the app itself (`bin/rails runner` resolves
11
+ database.yml + credentials inside the app process; yamine never parses
12
+ config or touches a key) and provisions every database it declares:
13
+ each name gains a collision-guarded per-worktree suffix, the claim
14
+ records all names plus the server coordinates, schemas load once, and
15
+ `remove`/`clean` drop the entire set as a unit — orphaned worktrees
16
+ drop from the claim alone, without the app even booting. Boot injects
17
+ `DATABASE_URL` plus `NAME_DATABASE_URL` per configuration (Rails' own
18
+ convention), so supervised processes are isolated even in apps that
19
+ never heard of yamine. New surface: `yamine db describe` prints what
20
+ the app actually resolves (passwords masked), `db list` and
21
+ `worktree list` show every database under a claim, and `db create`
22
+ re-probes — run it after the app grows a database to self-heal every
23
+ worktree.
24
+ - **`.yamine-db-suffix` — hand-run commands isolate too.** `add` writes
25
+ a token file (git-excluded automatically); a four-line suffix hook at
26
+ the top of `config/local.yml`'s sibling `database.yml` (see README)
27
+ makes a hand-run `rails console`, `rails test`, or `db:migrate` in
28
+ the worktree resolve to the worktree's databases — env injection only
29
+ ever reaches processes yamine spawns. Conformance is proven at
30
+ creation ("database.yml reads .yamine-db-suffix"); an app without the
31
+ hook still gets fully isolated boots via env, and gets told exactly
32
+ what the gap is at that moment.
33
+ - **The test database joins the claim.** It is probed and suffixed like
34
+ the rest, schema-prepared on first creation (`db:test:prepare` — Rails
35
+ will not load schema into an empty test database by itself), and
36
+ dropped at teardown — a fresh worktree runs `rails test` as-is, and
37
+ no suffixed test database is ever left behind.
38
+ - **Credential keys travel to worktrees.** `add` now copies
39
+ `config/master.key` and `config/credentials/*.key` (mode 0600) with
40
+ the rest of the per-checkout config — a credentials app could not
41
+ even boot in a fresh worktree before, let alone resolve its
42
+ databases.
43
+
44
+ ### Changed
45
+
46
+ - **The main checkout of a Rails app boots in silence.** The
47
+ "app looks database-backed but no DATABASE_URL template found"
48
+ warning no longer fires on a main checkout — the app's own databases
49
+ ARE its databases; isolation is a worktree concern, and worktrees
50
+ get it from the probe.
51
+ - **`clean` no longer gets stuck forever on unresolvable legacy
52
+ claims.** A pre-multi-database claim whose app declares no template
53
+ (credentials apps never did) could never be resolved by any future
54
+ command, so every `worktree clean` anywhere failed on it until it was
55
+ removed by hand. Such claims are now forgotten with a warning that
56
+ names the database, so a real leftover stays droppable by hand while
57
+ `clean` moves on.
58
+ - The claims file (`databases.json`) is written mode 0600 —
59
+ multi-database claims carry the app's resolved connection URLs.
60
+
61
+ ### Fixed
62
+
63
+ - **Every real Postgres drop has always crashed.** `pg_drop` passed
64
+ the environment as a keyword to `Open3.capture2` (spawn takes it
65
+ positionally), so the moment a `dropdb` was actually attempted —
66
+ `worktree remove`, `worktree clean`, any teardown against a reachable
67
+ server — it raised `ArgumentError` mid-teardown, after routes were
68
+ already stopped. Unit tests never saw it (an unreachable server
69
+ returns before the spawn). Drops now run, verified against a real
70
+ five-database app end to end.
71
+ - **Boots of dependency-less Node stubs failed forever.** A committed
72
+ `package.json` with no dependencies (`{}`) demanded a `node_modules`
73
+ directory that `npm install` itself will never create — every fresh
74
+ clone and worktree of such an app failed its pre-flight. The check
75
+ now fires only when the manifest actually declares dependencies (an
76
+ unparseable manifest keeps the old conservative behavior).
77
+ - **A hostname with no route answers 503, not 404.** The proxy was
78
+ answering 404 for "no app registered for this hostname" — the same status
79
+ an app gives for a path it does not have. A machine client (a health
80
+ check, an API client, an agent) that trusts the status concluded the app
81
+ had answered and went looking for a bug in the app's routes, when the app
82
+ was not running at all. It now answers 503 Service Unavailable with the
83
+ same helpful page (parent app, its directory, `yamine start`), so the
84
+ status alone says the app is not there — matching the 502 a registered
85
+ route with a dead backend already gets. Foreign hosts keep their bare 404
86
+ that names nothing.
87
+
3
88
  ## [0.15.1] — 2026-09-22
4
89
 
5
90
  ### Fixed
data/README.md CHANGED
@@ -170,9 +170,11 @@ yamine worktree clean # tear down everything already merged
170
170
  ```
171
171
 
172
172
  `add` lands the worktree beside the repo, copies the gitignored
173
- per-checkout config (`config/local.yml`, `config/local.secrets`) the
174
- branch needs, runs `bundle install`, and pre-creates the per-worktree
175
- database with schema — the next step is just `yamine start` in it.
173
+ per-checkout config (`config/local.yml`, `config/local.secrets`,
174
+ `config/master.key`, `config/credentials/*.key`) the branch needs, runs
175
+ `bundle install`, asks the app what databases it has, and provisions
176
+ the whole set with schema — the next step is just `yamine start` in
177
+ it.
176
178
 
177
179
  `clean` is the done-and-merged sweep: it tears down every worktree
178
180
  whose branch is merged (stop, drop database, remove worktree, delete
@@ -190,6 +192,76 @@ yamine --variant demo # -> https://demo.myapp.localhos
190
192
  yamine --tld preview.example.com # your own domain (OAuth parity)
191
193
  ```
192
194
 
195
+ ### Multi-database apps
196
+
197
+ A Rails multi-database app (five databases is normal: primary, cache,
198
+ queue, cable, errors) gets **the whole set** per worktree — asked, not
199
+ guessed. `worktree add` boots `bin/rails runner` once inside the app so
200
+ database.yml and credentials resolve exactly as the app would resolve
201
+ them (yamine never parses config or touches a key), suffixes every
202
+ database name with a collision-guarded per-worktree token, creates and
203
+ schema-loads them, and records names plus server coordinates in the
204
+ claim. Boot injects `DATABASE_URL` and one `NAME_DATABASE_URL` per
205
+ configuration (Rails' own convention), so supervised processes are
206
+ isolated even in an app that never heard of yamine. `remove`/`clean`
207
+ drop the entire set as a unit — including from an orphaned claim whose
208
+ directory is already gone, without the app booting.
209
+
210
+ ```bash
211
+ yamine db describe # what THIS checkout resolves to (passwords masked)
212
+ yamine db list # every claim, every database under it
213
+ yamine db create # re-probe + provision (run after the app grows a database)
214
+ ```
215
+
216
+ **Hand-run commands.** Env injection only reaches processes yamine
217
+ spawns; a `rails console`, `rails test`, or `db:migrate` you run by
218
+ hand reads the environment you gave it. For those, opt the app in with
219
+ the suffix hook — a one-time addition at the top of
220
+ `config/database.yml`:
221
+
222
+ ```erb
223
+ <%
224
+ yamine_suffix = begin
225
+ f = Rails.root.join(".yamine-db-suffix")
226
+ f.exist? ? f.read.strip : ""
227
+ rescue StandardError
228
+ ""
229
+ end
230
+ yamine_db_url = lambda do |url|
231
+ next url if yamine_suffix.empty? || url.nil? || url.to_s.empty?
232
+ require "uri"
233
+ begin
234
+ uri = URI.parse(url.to_s)
235
+ uri.path = "#{uri.path}_#{yamine_suffix}" if uri.path && uri.path != "/"
236
+ uri.to_s
237
+ rescue URI::InvalidURIError
238
+ url
239
+ end
240
+ end
241
+ %>
242
+ ```
243
+
244
+ Then wrap each database URL (and a bare test database name) with it:
245
+
246
+ ```yaml
247
+ development:
248
+ primary:
249
+ url: <%= yamine_db_url.call(Rails.application.credentials.dig(:database, :primary, :url)) %>
250
+ test:
251
+ database: <%= ENV.fetch("TEST_DATABASE_NAME") { "myapp_test#{yamine_suffix.empty? ? "" : "_#{yamine_suffix}"}" } %>
252
+ ```
253
+
254
+ `worktree add` writes the `.yamine-db-suffix` token (git-excluded
255
+ automatically) and then *proves* the hook works — you'll see
256
+ `database.yml reads .yamine-db-suffix — hand-run commands are isolated
257
+ too`. Without the hook, supervised boots are still fully isolated via
258
+ env; `add` says exactly what the hand-run gap is. The test database is
259
+ part of the claim too: created and schema-prepared on first add (so
260
+ `rails test` runs as-is), dropped at teardown — never left behind.
261
+
262
+ The main checkout has no marker file, so nothing changes there: its
263
+ databases are its databases.
264
+
193
265
  ## Subdomains are opt-in
194
266
 
195
267
  A route answers its exact hostname. `*.myapp.localhost` reaches
@@ -207,8 +279,9 @@ yamine alias tenant1 4001 --wildcard # one route, its subdomains
207
279
  Off is the useful default. An unregistered label under a live app is far
208
280
  more likely to be a worktree whose stack is stopped than a tenant, and
209
281
  handing that label to the parent app means HTTP 200 with the wrong code.
210
- Instead the request 404s and names the parent app, its directory, and how
211
- to start it. `yamine status` reports which mode an app is in.
282
+ Instead the request fails with 503 and names the parent app, its directory,
283
+ and how to start it — the app is not there, which is not the same statement
284
+ as the app answering 404. `yamine status` reports which mode an app is in.
212
285
 
213
286
  ## Commands
214
287
 
@@ -225,7 +298,7 @@ yamine open [name] # open the app URL in a browser
225
298
  yamine trust # add local CA to system trust store
226
299
  yamine clean # remove state and hosts entries
227
300
  yamine prune # remove stale routes
228
- yamine db list|create|drop # per-worktree databases
301
+ yamine db list|create|drop|describe # per-worktree databases (multi-database aware)
229
302
  yamine worktree list|add|remove|clean # worktree lifecycle
230
303
  yamine stop # stop this app's backend + routes
231
304
  yamine restart # touch tmp/restart.txt (managed apps reboot)
@@ -108,26 +108,47 @@ yamine worktree clean [--dry-run]
108
108
  ```
109
109
 
110
110
  `add` creates the git worktree beside the repo, copies the gitignored
111
- per-checkout config (`config/local.yml`, `config/local.secrets`), runs
112
- `bundle install`, and pre-creates the per-worktree database — then boot
113
- with `yamine start` inside it. `clean` tears down every worktree whose
114
- branch is merged (stops backends, drops the database, removes the
115
- worktree, deletes the branch) and forgets claims of directories that
116
- vanished. It never touches uncommitted work; unmerged branches survive
111
+ per-checkout config (`config/local.yml`, `config/local.secrets`,
112
+ credential keys), runs `bundle install`, **asks the app what databases
113
+ it has** (one `bin/rails runner` probe — database.yml and credentials
114
+ resolve inside the app; yamine never parses them), and provisions the
115
+ whole set with schema — every database of a multi-database app gets a
116
+ per-worktree suffix, the test database included and schema-prepared, so
117
+ `rails test` runs as-is. Then boot with `yamine start` inside it.
118
+ `remove` and `clean` drop the entire set as a unit — including from an
119
+ orphaned claim whose directory is gone, without booting the app — and
120
+ `clean` never touches uncommitted work; unmerged branches survive
117
121
  everything except `remove --force`. Prefer `clean --dry-run` first, and
118
- `clean` over `rm -rf` — a removed worktree leaves no routes, database,
122
+ `clean` over `rm -rf` — a removed worktree leaves no routes, databases,
119
123
  or stale hosts entries behind.
120
124
 
125
+ `yamine db describe` shows what the current checkout resolves to
126
+ (passwords masked); `yamine db list` shows every database under every
127
+ claim; `yamine db create` re-probes and self-heals (run it in each
128
+ worktree after the app grows a database).
129
+
130
+ Supervised boots are isolated via injected `DATABASE_URL` /
131
+ `NAME_DATABASE_URL` env vars. **Hand-run commands are not** — unless
132
+ the app carries the `.yamine-db-suffix` hook from the yamine README at
133
+ the top of `config/database.yml`. `worktree add` writes that token
134
+ file and prints `database.yml reads .yamine-db-suffix` when the hook
135
+ is in place; if instead you see a warning that database.yml does not
136
+ read it, hand-run `rails console` / `rails test` / `db:migrate` in
137
+ that worktree would use the main checkout's databases — add the hook
138
+ from the README, then `yamine db create`.
139
+
121
140
  ## Subdomains are opt-in
122
141
 
123
142
  A route answers its exact hostname. `*.myapp.localhost` reaches the app
124
143
  only if it asked (`proxy.subdomains: true` in `config/local.yml`, or
125
144
  `yamine alias <name> <port> --wildcard` for one route).
126
145
 
127
- So a worktree whose stack is not running gets a 404 that names the parent
128
- app, its directory, and `yamine start` — not the parent app's code. If
129
- you hit a `.localhost` URL that loads but looks wrong, check you started
130
- the worktree you think you did; `yamine status` in it prints the URL.
146
+ So a worktree whose stack is not running gets a 503 that names the parent
147
+ app, its directory, and `yamine start` — not the parent app's code. The
148
+ status says the app is not there, so a script can tell "not running" from
149
+ "the app answered 404". If you hit a `.localhost` URL that loads but looks
150
+ wrong, check you started the worktree you think you did; `yamine status` in
151
+ it prints the URL.
131
152
 
132
153
  ## First time on a machine
133
154