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 +4 -4
- data/CHANGELOG.md +85 -0
- data/README.md +79 -6
- data/lib/ask/skills/yamine/SKILL.md +32 -11
- data/lib/yamine/cli/boot.rb +282 -42
- data/lib/yamine/cli/system.rb +105 -26
- data/lib/yamine/cli/worktree.rb +142 -46
- data/lib/yamine/cli.rb +1 -1
- data/lib/yamine/database.rb +131 -4
- data/lib/yamine/probe.rb +132 -0
- data/lib/yamine/proxy.rb +6 -1
- data/lib/yamine/readiness.rb +27 -2
- data/lib/yamine/runner.rb +18 -10
- data/lib/yamine/version.rb +1 -1
- data/lib/yamine.rb +1 -0
- metadata +2 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 66abd82532420b13b18597a4d71b51485f06b09c14d5b0d4c3e777d8056e7145
|
|
4
|
+
data.tar.gz: be3bcb4b88ba0eeb444ed33d10c14c217cf145073f2f8cce78e62e4dc138bffd
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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
|
|
174
|
-
branch needs, runs
|
|
175
|
-
|
|
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
|
|
211
|
-
to start it
|
|
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
|
|
112
|
-
`bundle install`,
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
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,
|
|
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
|
|
128
|
-
app, its directory, and `yamine start` — not the parent app's code.
|
|
129
|
-
|
|
130
|
-
the
|
|
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
|
|