yamine 0.16.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/CHANGELOG.md +67 -0
- data/README.md +78 -54
- data/lib/ask/skills/yamine/SKILL.md +29 -21
- data/lib/yamine/cli/boot.rb +68 -39
- data/lib/yamine/cli/system.rb +1 -1
- data/lib/yamine/cli/worktree.rb +18 -14
- data/lib/yamine/database.rb +89 -18
- data/lib/yamine/probe.rb +5 -1
- data/lib/yamine/version.rb +1 -1
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 43b8c059b0635ce4272b806fd2b8f70ae42ed6d591ed87536de9c86ed0419864
|
|
4
|
+
data.tar.gz: c34fb3a40d5c16442accee4dcbdb32b1b2b5013de5c3a823bade745b6ac65e88
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 70495a26f226c3b0dac853c5a4ce1694207f4a9bbb73af92b44b37cc17ff3c13430af7440252102b8acd40f22ee7efdae5dfec26155678a5839e30b10c5c0d1a
|
|
7
|
+
data.tar.gz: e450abc81d00dad86bf03e540304ad43683e99329a60eb24ddea0122760048c88729e7fe33c9b93056a1840ec871e488ea458d1afdbd655b0dccd477369b8cb5
|
data/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,73 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
+
## [0.18.0] — 2026-09-23
|
|
6
|
+
|
|
7
|
+
### Changed
|
|
8
|
+
|
|
9
|
+
- **Only environment-scoped env files — never plain `.env`.**
|
|
10
|
+
`worktree add` writes `.env.development` + `.env.test`; the plain
|
|
11
|
+
`.env` it also wrote in 0.17.0 is gone. Plain `.env` is the file
|
|
12
|
+
production-style tooling reads by name — kamal, docker
|
|
13
|
+
`--env-file`, and dotenv itself loads `.env` in *every* environment
|
|
14
|
+
— so a worktree's copy could leak development database URLs into a
|
|
15
|
+
production boot that happened to run from that directory.
|
|
16
|
+
`.env.development` / `.env.test` are invisible to a production boot
|
|
17
|
+
by construction. A yamine-authored `.env` from 0.17.0 is cleaned up
|
|
18
|
+
on sight (header-marked); a foreign `.env` — app-committed or
|
|
19
|
+
hand-written — is never touched.
|
|
20
|
+
- **Env-file writes upsert instead of overwrite.** yamine's keys are
|
|
21
|
+
(re)written; every other line in the file survives, so a committed
|
|
22
|
+
env file's config or a developer's copied-in API keys are no longer
|
|
23
|
+
destroyed by `yamine db create`. The yamine header stays
|
|
24
|
+
idempotent across re-runs.
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
## [0.17.0] — 2026-09-23
|
|
28
|
+
|
|
29
|
+
### Added
|
|
30
|
+
|
|
31
|
+
- **Per-worktree `.env` files — hand-run isolation without touching the
|
|
32
|
+
app's config.** `worktree add` writes `.env` and `.env.development`
|
|
33
|
+
(the whole development set: `DATABASE_URL` + one `NAME_DATABASE_URL`
|
|
34
|
+
per configuration) and `.env.test` (`PRIMARY_DATABASE_URL` — the key
|
|
35
|
+
Rails checks *before* `DATABASE_URL` for a flat test config, so the
|
|
36
|
+
test URL wins no matter what order a loader reads the files in).
|
|
37
|
+
Any dotenv loader picks them up — hand-run `rails console`,
|
|
38
|
+
`rails test`, and `db:migrate` land on the worktree's databases with
|
|
39
|
+
zero yamine-specific code in `database.yml`. Mode 0600, git-excluded
|
|
40
|
+
automatically, removed with the worktree.
|
|
41
|
+
|
|
42
|
+
### Changed
|
|
43
|
+
|
|
44
|
+
- **`.yamine-db-suffix` (0.16.0) is replaced by the `.env` files above.**
|
|
45
|
+
The database.yml suffix hook is no longer needed — apps keep a plain
|
|
46
|
+
boring config. Legacy marker files are deleted on sight.
|
|
47
|
+
- **The conformance check now diagnoses precisely.** `worktree add`
|
|
48
|
+
re-probes after writing `.env` and distinguishes the two ways an app
|
|
49
|
+
can fail to isolate hand-run commands: no dotenv loader loaded the
|
|
50
|
+
file (fix: one Gemfile line), vs `database.yml` supplies `url:` keys,
|
|
51
|
+
which Rails gives *precedence over the entire environment* —
|
|
52
|
+
`merge_db_environment_variables` skips URL-shaped configs, so neither
|
|
53
|
+
injected env nor `.env` can ever redirect them (fix: component form —
|
|
54
|
+
`database:`, `host:`, `username:` — for development/test).
|
|
55
|
+
- **Worktrees copy only the credential keys their own environments
|
|
56
|
+
need**: `config/master.key`, `config/credentials/development.key`,
|
|
57
|
+
`config/credentials/test.key`. Production and staging keys no longer
|
|
58
|
+
travel into throwaway directories.
|
|
59
|
+
|
|
60
|
+
### Fixed
|
|
61
|
+
|
|
62
|
+
- **`worktree add` could purge the MAIN checkout's test database.**
|
|
63
|
+
The per-worktree test schema preparation ran under
|
|
64
|
+
`RAILS_ENV=development`, where the flat test config resolves to the
|
|
65
|
+
BASE name (the environment-override merge only applies to the current
|
|
66
|
+
environment's configs) — so `db:test:prepare` purged and reloaded
|
|
67
|
+
*main's* `myrr_markdown_test` during a worktree add, observed live.
|
|
68
|
+
It now runs under `RAILS_ENV=test`, where dotenv loads `.env.test`
|
|
69
|
+
and the override lands on the worktree's own test database; a
|
|
70
|
+
regression test pins the environment.
|
|
71
|
+
|
|
5
72
|
## [0.16.0] — 2026-09-23
|
|
6
73
|
|
|
7
74
|
### Added
|
data/README.md
CHANGED
|
@@ -171,7 +171,7 @@ yamine worktree clean # tear down everything already merged
|
|
|
171
171
|
|
|
172
172
|
`add` lands the worktree beside the repo, copies the gitignored
|
|
173
173
|
per-checkout config (`config/local.yml`, `config/local.secrets`,
|
|
174
|
-
`config/master.key`,
|
|
174
|
+
`config/master.key`, and the development/test credential keys) the branch needs, runs
|
|
175
175
|
`bundle install`, asks the app what databases it has, and provisions
|
|
176
176
|
the whole set with schema — the next step is just `yamine start` in
|
|
177
177
|
it.
|
|
@@ -200,12 +200,12 @@ guessed. `worktree add` boots `bin/rails runner` once inside the app so
|
|
|
200
200
|
database.yml and credentials resolve exactly as the app would resolve
|
|
201
201
|
them (yamine never parses config or touches a key), suffixes every
|
|
202
202
|
database name with a collision-guarded per-worktree token, creates and
|
|
203
|
-
schema-loads them, and records names plus
|
|
204
|
-
claim. Boot injects `DATABASE_URL` and one
|
|
205
|
-
configuration (Rails' own convention)
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
203
|
+
schema-loads them, prepares the test database, and records names plus
|
|
204
|
+
server coordinates in the claim. Boot injects `DATABASE_URL` and one
|
|
205
|
+
`NAME_DATABASE_URL` per configuration (Rails' own convention).
|
|
206
|
+
`remove`/`clean` drop the entire set as a unit — including from an
|
|
207
|
+
orphaned claim whose directory is already gone, without the app
|
|
208
|
+
booting.
|
|
209
209
|
|
|
210
210
|
```bash
|
|
211
211
|
yamine db describe # what THIS checkout resolves to (passwords masked)
|
|
@@ -213,54 +213,78 @@ yamine db list # every claim, every database under it
|
|
|
213
213
|
yamine db create # re-probe + provision (run after the app grows a database)
|
|
214
214
|
```
|
|
215
215
|
|
|
216
|
-
**
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
`
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
test
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
`worktree add` writes the
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
env
|
|
259
|
-
|
|
260
|
-
|
|
216
|
+
**Two one-time app requirements, both boring:**
|
|
217
|
+
|
|
218
|
+
1. *Component-form development/test config.* Environment overrides —
|
|
219
|
+
injected `DATABASE_URL`/`NAME_DATABASE_URL` and the per-worktree
|
|
220
|
+
`.env` — only apply to component keys (`database:`, `host:`). A
|
|
221
|
+
`url:` key (the usual credentials-driven style) takes precedence
|
|
222
|
+
over the *entire* environment: Rails skips URL-shaped configs when
|
|
223
|
+
merging environment variables, so nothing injected or loaded can
|
|
224
|
+
redirect them. Development and test should read like a plain Rails
|
|
225
|
+
file:
|
|
226
|
+
|
|
227
|
+
```yaml
|
|
228
|
+
default: &default
|
|
229
|
+
adapter: postgresql
|
|
230
|
+
encoding: unicode
|
|
231
|
+
host: <%= ENV.fetch("DB_HOST", "localhost") %>
|
|
232
|
+
username: postgres
|
|
233
|
+
|
|
234
|
+
development:
|
|
235
|
+
primary:
|
|
236
|
+
<<: *default
|
|
237
|
+
database: myapp_development
|
|
238
|
+
cache:
|
|
239
|
+
<<: *default
|
|
240
|
+
database: myapp_development_cache
|
|
241
|
+
migrations_paths: db/cache_migrate
|
|
242
|
+
|
|
243
|
+
staging:
|
|
244
|
+
primary: &primary_staging
|
|
245
|
+
<<: *default
|
|
246
|
+
url: <%= Rails.application.credentials.dig(:database, :primary, :url) %>
|
|
247
|
+
```
|
|
248
|
+
|
|
249
|
+
Staging/production keep doing whatever they do — yamine only ever
|
|
250
|
+
redirects development and test.
|
|
251
|
+
|
|
252
|
+
2. *A dotenv loader* — `gem "dotenv-rails", groups: [:development, :test]`
|
|
253
|
+
(any dotenv loader works). That is what reads the files
|
|
254
|
+
`worktree add` writes into the worktree:
|
|
255
|
+
|
|
256
|
+
- `.env.development` — the whole development set (`DATABASE_URL`
|
|
257
|
+
plus one `NAME_DATABASE_URL` per configuration),
|
|
258
|
+
- `.env.test` — the test URL under `PRIMARY_DATABASE_URL`, the key
|
|
259
|
+
Rails checks *before* `DATABASE_URL` for a flat test config, so it
|
|
260
|
+
wins regardless of the order a loader reads the files in.
|
|
261
|
+
|
|
262
|
+
Only these environment-scoped names are ever written — **never
|
|
263
|
+
plain `.env`**, the file production tooling reads by name (kamal,
|
|
264
|
+
docker `--env-file`, and dotenv itself loads `.env` in *every*
|
|
265
|
+
environment), so a production boot can never see a worktree's
|
|
266
|
+
database URLs. Mode 0600, git-excluded automatically, removed with
|
|
267
|
+
the worktree. Existing keys in those files are **upserted, never
|
|
268
|
+
clobbered** — your own entries (API keys, a committed env file's
|
|
269
|
+
config) survive `yamine db create`.
|
|
270
|
+
A hand-run `rails console`, `rails test`, or `db:migrate` in the
|
|
271
|
+
worktree therefore lands on the worktree's own databases — with
|
|
272
|
+
**zero yamine-specific code in `database.yml`, ever**.
|
|
273
|
+
|
|
274
|
+
`worktree add` proves both after writing the files: you will see
|
|
275
|
+
`.env loaded — hand-run commands are isolated too`. A warning instead
|
|
276
|
+
names which requirement is missing — no loader ran, or the config is
|
|
277
|
+
`url:`-shaped — and the exact fix. Supervised boots are isolated via
|
|
278
|
+
injected env either way (component form permitting).
|
|
279
|
+
|
|
280
|
+
Credential keys travel too: `config/master.key`,
|
|
281
|
+
`config/credentials/development.key`, and
|
|
282
|
+
`config/credentials/test.key` are copied at mode 0600. Production and
|
|
283
|
+
staging keys stay in the main checkout where they belong.
|
|
284
|
+
|
|
285
|
+
The main checkout never gets these env files or a suffix: its databases
|
|
286
|
+
are its databases, untouched.
|
|
261
287
|
|
|
262
|
-
The main checkout has no marker file, so nothing changes there: its
|
|
263
|
-
databases are its databases.
|
|
264
288
|
|
|
265
289
|
## Subdomains are opt-in
|
|
266
290
|
|
|
@@ -109,33 +109,41 @@ yamine worktree clean [--dry-run]
|
|
|
109
109
|
|
|
110
110
|
`add` creates the git worktree beside the repo, copies the gitignored
|
|
111
111
|
per-checkout config (`config/local.yml`, `config/local.secrets`,
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
`
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
112
|
+
`config/master.key`, and the development/test credential keys — never
|
|
113
|
+
production/staging keys), runs `bundle install`, **asks the app what
|
|
114
|
+
databases it has** (one `bin/rails runner` probe — database.yml and
|
|
115
|
+
credentials resolve inside the app; yamine never parses them), and
|
|
116
|
+
provisions the whole set with schema — every database of a
|
|
117
|
+
multi-database app gets a per-worktree suffix, the test database
|
|
118
|
+
included and schema-prepared, so `rails test` runs as-is. It writes
|
|
119
|
+
`.env.development` (the development set) and `.env.test`
|
|
120
|
+
(`PRIMARY_DATABASE_URL` — the key Rails checks first for a flat test
|
|
121
|
+
config) into the worktree, git-excluded automatically — never plain `.env`
|
|
122
|
+
(production tooling reads that name), and existing keys in those files
|
|
123
|
+
are upserted, not clobbered. Then boot with
|
|
124
|
+
`yamine start` inside it. `remove` and `clean` drop the entire set as
|
|
125
|
+
a unit — including from an orphaned claim whose directory is gone,
|
|
126
|
+
without booting the app — and `clean` never touches uncommitted work;
|
|
127
|
+
unmerged branches survive everything except `remove --force`. Prefer
|
|
128
|
+
`clean --dry-run` first, and `clean` over `rm -rf` — a removed
|
|
129
|
+
worktree leaves no routes, databases, or stale hosts entries behind.
|
|
124
130
|
|
|
125
131
|
`yamine db describe` shows what the current checkout resolves to
|
|
126
132
|
(passwords masked); `yamine db list` shows every database under every
|
|
127
133
|
claim; `yamine db create` re-probes and self-heals (run it in each
|
|
128
134
|
worktree after the app grows a database).
|
|
129
135
|
|
|
130
|
-
|
|
131
|
-
`NAME_DATABASE_URL` env vars
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
136
|
+
Isolation reaches two places: supervised boots via injected
|
|
137
|
+
`DATABASE_URL` / `NAME_DATABASE_URL` env vars, and hand-run commands
|
|
138
|
+
(`rails console`, `rails test`, `db:migrate`) via these env files —
|
|
139
|
+
which require a dotenv loader in the app (`gem "dotenv-rails",
|
|
140
|
+
groups: [:development, :test]`) and **component-form development/test
|
|
141
|
+
config** (`database:` keys, never `url:` — a `url:` key takes
|
|
142
|
+
precedence over the entire environment, so nothing injected or loaded
|
|
143
|
+
can redirect it; staging/production URLs are fine, yamine never
|
|
144
|
+
touches those envs). `worktree add` verifies both and prints
|
|
145
|
+
`.env loaded — hand-run commands are isolated too`; its warning names
|
|
146
|
+
exactly which requirement is missing and the fix.
|
|
139
147
|
|
|
140
148
|
## Subdomains are opt-in
|
|
141
149
|
|
data/lib/yamine/cli/boot.rb
CHANGED
|
@@ -586,26 +586,25 @@ module Yamine
|
|
|
586
586
|
# Worktree-add (and `yamine db create` in a worktree): turn a
|
|
587
587
|
# probe of the app's real databases into a provisioned,
|
|
588
588
|
# self-describing claim. Names come from the app, the suffix
|
|
589
|
-
# from the directory; claim +
|
|
589
|
+
# from the directory; claim + .env files are written BEFORE any
|
|
590
590
|
# database is created, so a failure mid-way leaves a state the
|
|
591
591
|
# next `yamine db create` or boot can finish from — never a
|
|
592
592
|
# half-named set with no record of it.
|
|
593
593
|
#
|
|
594
594
|
# Returns the final names; raises Error only for conditions the
|
|
595
|
-
# user must fix (name budget, server down,
|
|
596
|
-
#
|
|
595
|
+
# user must fix (name budget, server down, .env breaking the
|
|
596
|
+
# app's boot) — those keep the worktree and the claim.
|
|
597
597
|
def provision_multidb(ctx, dir, claim_key, rows)
|
|
598
598
|
env_name = rails_env
|
|
599
|
-
#
|
|
600
|
-
#
|
|
601
|
-
# fitting, or this pass would double it and
|
|
602
|
-
# provision a set the app never connects to.
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
end
|
|
599
|
+
# Re-provisioning a worktree whose .env exists: the app (via
|
|
600
|
+
# dotenv) resolves SUFFIXED names — strip the claim's stored
|
|
601
|
+
# suffix before fitting, or this pass would double it and
|
|
602
|
+
# yamine would provision a set the app never connects to.
|
|
603
|
+
prior = Database.load_map(ctx.store.dir)[claim_key]
|
|
604
|
+
prior_suffix = prior.is_a?(Hash) ? prior["suffix"] : nil
|
|
605
|
+
rows = strip_suffix(rows, prior_suffix)
|
|
607
606
|
# The test environment's database joins the claim: worktree
|
|
608
|
-
# tests run against it (
|
|
607
|
+
# tests run against it (.env.test carries its URL), and
|
|
609
608
|
# teardown must drop it — a leaked test database per worktree
|
|
610
609
|
# is exactly the leftover this exists to prevent. Test configs
|
|
611
610
|
# are flat (implicitly named "primary"), so the claim key is
|
|
@@ -613,7 +612,7 @@ module Yamine
|
|
|
613
612
|
# A test env the app cannot answer (missing test credentials,
|
|
614
613
|
# say) degrades to a dev-only claim; `yamine db create` heals.
|
|
615
614
|
test_rows = Probe.rails_databases(dir, env: "test")
|
|
616
|
-
test_rows =
|
|
615
|
+
test_rows = strip_suffix(test_rows, prior_suffix)
|
|
617
616
|
test_pairs = (test_rows && Probe.server_backed(test_rows) || [])
|
|
618
617
|
.map { |r| [env_namespaced_key(r["name"], "test"), r] }
|
|
619
618
|
|
|
@@ -638,8 +637,14 @@ module Yamine
|
|
|
638
637
|
"claimed_at" => Time.now.utc.iso8601,
|
|
639
638
|
"suffix" => fitted, "names" => names, "bases" => bases)
|
|
640
639
|
Database.save_map(ctx.store.dir, map)
|
|
641
|
-
|
|
642
|
-
|
|
640
|
+
# The environment files: what hand-run commands read. Written
|
|
641
|
+
# before creation so the verify probe below can see them.
|
|
642
|
+
dev_env = db_env_for(
|
|
643
|
+
names.reject { |cfg, _| cfg == "test" || cfg.start_with?("test_") }, bases)
|
|
644
|
+
test_url = names["test"] &&
|
|
645
|
+
Database.url_for(names["test"], base_for(bases, "test"))
|
|
646
|
+
Database.write_env_files(dir, dev_env, test_url: test_url)
|
|
647
|
+
Database.exclude_files(dir, Database::ENV_FILES)
|
|
643
648
|
|
|
644
649
|
# An empty test database is worse than none: Rails won't
|
|
645
650
|
# auto-load schema into it (verified — first `rails test`
|
|
@@ -659,8 +664,14 @@ module Yamine
|
|
|
659
664
|
end
|
|
660
665
|
|
|
661
666
|
if test_needs_schema
|
|
667
|
+
# RAILS_ENV=test, not development: dotenv must load .env.test
|
|
668
|
+
# (PRIMARY_DATABASE_URL = this worktree's test URL) AND the
|
|
669
|
+
# environment-override merge only applies to the CURRENT
|
|
670
|
+
# env's configs — under development the flat test config
|
|
671
|
+
# would resolve to the base name and purge+load the MAIN
|
|
672
|
+
# checkout's test database instead of this worktree's.
|
|
662
673
|
prepared = Dir.chdir(dir) do
|
|
663
|
-
system({ "RAILS_ENV" =>
|
|
674
|
+
system({ "RAILS_ENV" => "test" }, "sh", "-c", "bin/rails db:test:prepare",
|
|
664
675
|
out: File::NULL, err: File::NULL)
|
|
665
676
|
end
|
|
666
677
|
if prepared
|
|
@@ -670,16 +681,16 @@ module Yamine
|
|
|
670
681
|
end
|
|
671
682
|
end
|
|
672
683
|
|
|
673
|
-
|
|
684
|
+
verify_env_loading(dir, names)
|
|
674
685
|
names
|
|
675
686
|
end
|
|
676
687
|
|
|
677
|
-
def
|
|
678
|
-
return rows if
|
|
688
|
+
def strip_suffix(rows, suffix)
|
|
689
|
+
return rows if rows.nil? || suffix.nil? || suffix.empty?
|
|
679
690
|
|
|
680
691
|
rows.map do |r|
|
|
681
|
-
if r["database"].end_with?("_#{
|
|
682
|
-
r.merge("database" => r["database"].delete_suffix("_#{
|
|
692
|
+
if r["database"].end_with?("_#{suffix}")
|
|
693
|
+
r.merge("database" => r["database"].delete_suffix("_#{suffix}"))
|
|
683
694
|
else
|
|
684
695
|
r
|
|
685
696
|
end
|
|
@@ -692,20 +703,21 @@ module Yamine
|
|
|
692
703
|
cfg_name == "primary" ? env : "#{env}_#{cfg_name}"
|
|
693
704
|
end
|
|
694
705
|
|
|
695
|
-
# The proof, not the promise: re-probe now that
|
|
696
|
-
#
|
|
697
|
-
# hand-run console/test/
|
|
698
|
-
#
|
|
699
|
-
#
|
|
700
|
-
#
|
|
701
|
-
#
|
|
702
|
-
#
|
|
703
|
-
|
|
706
|
+
# The proof, not the promise: re-probe now that .env exists.
|
|
707
|
+
# Matching names mean something inside the app loads .env
|
|
708
|
+
# (dotenv-rails or any dotenv loader) — hand-run console/test/
|
|
709
|
+
# migrate commands are isolated too. Mismatch is not fatal
|
|
710
|
+
# (supervised boots isolate via injected env regardless), but
|
|
711
|
+
# it is the one gap worth shouting about at the exact moment of
|
|
712
|
+
# creation. A probe that cannot run at all after writing .env
|
|
713
|
+
# IS fatal: the app no longer boots, and .env's own authorship
|
|
714
|
+
# is the likely reason.
|
|
715
|
+
def verify_env_loading(dir, names)
|
|
704
716
|
rows = Probe.rails_databases(dir)
|
|
705
717
|
unless rows
|
|
706
718
|
raise Error,
|
|
707
|
-
"the app no longer boots after writing
|
|
708
|
-
"check the
|
|
719
|
+
"the app no longer boots after writing .env — " \
|
|
720
|
+
"check the generated .env files in #{dir}"
|
|
709
721
|
end
|
|
710
722
|
|
|
711
723
|
actual = rows.to_h { |r| [r["name"], r["database"]] }
|
|
@@ -714,20 +726,37 @@ module Yamine
|
|
|
714
726
|
# for them either way; skip, don't flag.
|
|
715
727
|
mismatched = names.reject { |cfg, want| !actual.key?(cfg) || actual[cfg] == want }
|
|
716
728
|
if mismatched.empty?
|
|
717
|
-
puts "
|
|
729
|
+
puts " .env loaded — hand-run commands are isolated too"
|
|
718
730
|
else
|
|
719
731
|
cfg, want = mismatched.first
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
732
|
+
env_url = rows.first["env_database_url"]
|
|
733
|
+
if env_url && !env_url.empty?
|
|
734
|
+
# The loader DID run — DATABASE_URL reached the app — yet
|
|
735
|
+
# resolution ignored it. database.yml supplies `url:` keys
|
|
736
|
+
# (credentials-style), and for those Rails gives the FILE
|
|
737
|
+
# precedence: merge_db_environment_variables skips configs
|
|
738
|
+
# that are already URL-shaped. No environment channel,
|
|
739
|
+
# spawned or loaded, can redirect this app in that form.
|
|
740
|
+
warn " WARNING: #{cfg} resolves to #{actual[cfg].inspect} even though " \
|
|
741
|
+
"DATABASE_URL says #{env_url.split("/").last.inspect}."
|
|
742
|
+
warn " database.yml's `url:` keys take precedence over the environment in this configuration —"
|
|
743
|
+
warn " neither injected env nor .env can redirect them. Switch development/test to"
|
|
744
|
+
warn " component form (database:, host:, username:) so DATABASE_URL and .env apply —"
|
|
745
|
+
warn " see the yamine README's multi-database section."
|
|
746
|
+
else
|
|
747
|
+
warn " WARNING: this app does not load .env " \
|
|
748
|
+
"(#{cfg} resolves to #{actual[cfg].inspect}, expected #{want.inspect})."
|
|
749
|
+
warn " Supervised boots are still isolated via injected env, but `rails console`, " \
|
|
750
|
+
"`rails test`, and `db:migrate` run by hand in this worktree would use the main checkout's databases."
|
|
751
|
+
warn " Fix once: add `gem \"dotenv-rails\", groups: [:development, :test]` to the Gemfile — " \
|
|
752
|
+
"any dotenv loader works."
|
|
753
|
+
end
|
|
725
754
|
end
|
|
726
755
|
rescue StandardError => e
|
|
727
756
|
raise Error, e.message if e.is_a?(Error)
|
|
728
757
|
|
|
729
758
|
raise Error,
|
|
730
|
-
"the app no longer boots after writing
|
|
759
|
+
"the app no longer boots after writing .env: #{e.message}"
|
|
731
760
|
end
|
|
732
761
|
|
|
733
762
|
# Top-level `db: false` opts out of per-worktree databases.
|
data/lib/yamine/cli/system.rb
CHANGED
|
@@ -930,7 +930,7 @@ module Yamine
|
|
|
930
930
|
end
|
|
931
931
|
|
|
932
932
|
# Main checkout: the databases ARE the app's own — no suffix, no
|
|
933
|
-
#
|
|
933
|
+
# .env files, no claim names. Just make sure they exist.
|
|
934
934
|
def db_create_main(rows)
|
|
935
935
|
missing = rows.reject { |r| Database.exists?(r["database"], r["url"]) }
|
|
936
936
|
failed = missing.filter_map do |r|
|
data/lib/yamine/cli/worktree.rb
CHANGED
|
@@ -29,19 +29,23 @@ module Yamine
|
|
|
29
29
|
# credentials apps) no decryption key, which stops Rails before it
|
|
30
30
|
# can even read database.yml. The single largest follow-up cost of
|
|
31
31
|
# a new worktree, now carried automatically.
|
|
32
|
-
LOCAL_CONFIG_FILES = %w[config/local.yml config/local.secrets
|
|
33
|
-
#
|
|
34
|
-
#
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
32
|
+
LOCAL_CONFIG_FILES = %w[config/local.yml config/local.secrets].freeze
|
|
33
|
+
# Only the credential keys the worktree's own environments need:
|
|
34
|
+
# master (multi-env credentials), development (boots + probe),
|
|
35
|
+
# test (test runs). Production and staging keys have no business
|
|
36
|
+
# sitting in a throwaway worktree directory.
|
|
37
|
+
CREDENTIAL_KEY_FILES = %w[
|
|
38
|
+
config/master.key
|
|
39
|
+
config/credentials/development.key
|
|
40
|
+
config/credentials/test.key
|
|
41
|
+
].freeze
|
|
42
|
+
|
|
43
|
+
# Everything yamine copies or writes that must never read as
|
|
44
|
+
# uncommitted work: the per-checkout config, the three allowed
|
|
45
|
+
# keys, and the generated environment files.
|
|
46
|
+
def local_config_files(_dir)
|
|
47
|
+
LOCAL_CONFIG_FILES + CREDENTIAL_KEY_FILES + Database::ENV_FILES +
|
|
48
|
+
[Database::LEGACY_MARKER_FILE]
|
|
45
49
|
end
|
|
46
50
|
|
|
47
51
|
def run(ctx, args)
|
|
@@ -198,7 +202,7 @@ module Yamine
|
|
|
198
202
|
# app's schema) so server problems surface at creation time, not
|
|
199
203
|
# at first boot. Rails apps are ASKED what they have (the probe
|
|
200
204
|
# resolves database.yml + credentials inside the app itself) and
|
|
201
|
-
# get the whole suffixed set — claim,
|
|
205
|
+
# get the whole suffixed set — claim, .env files, databases,
|
|
202
206
|
# schema. Boot reuses an existing set idempotently, so this is
|
|
203
207
|
# never wasted work. SQLite and non-Rails apps fall through to
|
|
204
208
|
# the single-database path, which setup_database narrates.
|
data/lib/yamine/database.rb
CHANGED
|
@@ -32,12 +32,29 @@ module Yamine
|
|
|
32
32
|
|
|
33
33
|
MAX_IDENTIFIER_BYTES = 63
|
|
34
34
|
STATE_FILE = "databases.json"
|
|
35
|
-
#
|
|
36
|
-
#
|
|
37
|
-
#
|
|
38
|
-
#
|
|
39
|
-
#
|
|
40
|
-
|
|
35
|
+
# The per-worktree environment files yamine writes — ONLY
|
|
36
|
+
# environment-scoped names, never plain `.env`. Production-style
|
|
37
|
+
# tooling reads `.env` by name (kamal, docker --env-file, and
|
|
38
|
+
# dotenv itself loads `.env` in *every* environment), so a
|
|
39
|
+
# worktree's plain `.env` could leak dev database URLs into a
|
|
40
|
+
# production boot that happened to run from this directory.
|
|
41
|
+
# `.env.development` / `.env.test` are invisible to a production
|
|
42
|
+
# boot by construction — dotenv only ever reads
|
|
43
|
+
# `.env.#{Rails.env}` for the current environment.
|
|
44
|
+
#
|
|
45
|
+
# `.env.test` carries PRIMARY_DATABASE_URL — the key Rails checks
|
|
46
|
+
# *before* DATABASE_URL for a flat test config, so the test URL
|
|
47
|
+
# wins no matter which order a loader reads the files in. Hand-run
|
|
48
|
+
# commands (`rails console`, `rails test`, `db:migrate`) pick
|
|
49
|
+
# these up; env injection still covers processes yamine spawns.
|
|
50
|
+
ENV_FILES = %w[.env.development .env.test].freeze
|
|
51
|
+
# 0.16.0's marker file — removed in favor of env files; deleted on
|
|
52
|
+
# sight so upgraded worktrees don't accumulate dead files.
|
|
53
|
+
LEGACY_MARKER_FILE = ".yamine-db-suffix"
|
|
54
|
+
# 0.17.0 wrote plain `.env` too; this header marks it as ours to
|
|
55
|
+
# delete. A `.env` WITHOUT the header belongs to the app or the
|
|
56
|
+
# developer and is never touched.
|
|
57
|
+
LEGACY_ENV_HEADER = "written by yamine"
|
|
41
58
|
|
|
42
59
|
# Database name for a worktree dir + env (development/test).
|
|
43
60
|
# Main checkout (no worktree marker) keeps the bare name.
|
|
@@ -171,28 +188,82 @@ module Yamine
|
|
|
171
188
|
name
|
|
172
189
|
end
|
|
173
190
|
|
|
174
|
-
|
|
175
|
-
|
|
191
|
+
# Write the worktree's environment files (see ENV_FILES for the
|
|
192
|
+
# why of the names). Upsert semantics: our keys are (re)written,
|
|
193
|
+
# every other line in the file survives — a committed env file's
|
|
194
|
+
# non-database config, or a developer's copied-in API keys, must
|
|
195
|
+
# not be destroyed by `yamine db create`. Private mode; the
|
|
196
|
+
# 0.16.0 marker and a yamine-authored 0.17.0 `.env` are removed
|
|
197
|
+
# on the way past.
|
|
198
|
+
def write_env_files(dir, dev_env, test_url: nil)
|
|
199
|
+
FileUtils.rm_f(File.join(dir, LEGACY_MARKER_FILE))
|
|
200
|
+
remove_legacy_worktree_env(dir)
|
|
201
|
+
return if dev_env.nil? && test_url.nil?
|
|
202
|
+
|
|
203
|
+
unless dev_env.nil? || dev_env.empty?
|
|
204
|
+
merge_env_file(File.join(dir, ".env.development"),
|
|
205
|
+
dev_env.reject { |k, _v| k == "PRIMARY_DATABASE_URL" })
|
|
206
|
+
end
|
|
207
|
+
return if test_url.nil? || test_url.to_s.empty?
|
|
208
|
+
|
|
209
|
+
merge_env_file(File.join(dir, ".env.test"),
|
|
210
|
+
{ "PRIMARY_DATABASE_URL" => test_url })
|
|
211
|
+
end
|
|
212
|
+
|
|
213
|
+
# 0.17.0 wrote plain `.env` — remove it ONLY when yamine authored
|
|
214
|
+
# it; an app-committed or developer-written `.env` is theirs.
|
|
215
|
+
def remove_legacy_worktree_env(dir)
|
|
216
|
+
path = File.join(dir, ".env")
|
|
217
|
+
return unless File.file?(path)
|
|
218
|
+
return unless File.read(path).include?(LEGACY_ENV_HEADER)
|
|
219
|
+
|
|
220
|
+
FileUtils.rm_f(path)
|
|
221
|
+
end
|
|
222
|
+
|
|
223
|
+
# Upsert our keys into a dotenv file, preserving every other line.
|
|
224
|
+
# Our header is placed once (idempotent across re-runs).
|
|
225
|
+
def merge_env_file(path, env)
|
|
226
|
+
existing = File.file?(path) ? File.read(path).lines : []
|
|
227
|
+
key_res = env.keys.map { |k| /\A\s*#{Regexp.escape(k)}\s*=/ }
|
|
228
|
+
kept = existing.reject { |line| key_res.any? { |re| line.match?(re) } }
|
|
229
|
+
unless kept.any? { |line| line.include?(LEGACY_ENV_HEADER) }
|
|
230
|
+
kept = yamine_env_header.lines + kept
|
|
231
|
+
end
|
|
232
|
+
body = kept.join
|
|
233
|
+
body += "\n" unless body.empty? || body.end_with?("\n")
|
|
234
|
+
body += env.map { |k, v| "#{k}='#{dotenv_escape(v.to_s)}'\n" }.join
|
|
235
|
+
File.write(path, body)
|
|
236
|
+
File.chmod(0o600, path)
|
|
176
237
|
end
|
|
177
238
|
|
|
178
|
-
def
|
|
179
|
-
|
|
180
|
-
|
|
239
|
+
def yamine_env_header
|
|
240
|
+
"# Per-worktree databases, written by yamine (worktree add).\n" \
|
|
241
|
+
"# Environment-scoped (.env.development/.env.test) so a production\n" \
|
|
242
|
+
"# boot never reads these; other keys in this file are preserved.\n"
|
|
181
243
|
end
|
|
182
244
|
|
|
183
|
-
#
|
|
184
|
-
#
|
|
185
|
-
#
|
|
186
|
-
def
|
|
245
|
+
# dotenv format: single-quoted values (literal — no interpolation
|
|
246
|
+
# of a password's `$` or `#`), with the two escapes the dotenv
|
|
247
|
+
# grammar allows inside them.
|
|
248
|
+
def dotenv_escape(value)
|
|
249
|
+
value.gsub("\\") { "\\\\" }.gsub("'") { "\\'" }
|
|
250
|
+
end
|
|
251
|
+
|
|
252
|
+
# Keep files out of `git status` without touching the committed
|
|
253
|
+
# .gitignore: git's exclude file lives in the shared git dir, so
|
|
254
|
+
# entries cover every worktree of the repo. Idempotent per line.
|
|
255
|
+
def exclude_files(dir, names)
|
|
187
256
|
out, status = Open3.capture2("git", "-C", dir, "rev-parse", "--git-path", "info/exclude")
|
|
188
257
|
return unless status.success?
|
|
189
258
|
|
|
190
259
|
path = out.strip
|
|
191
260
|
path = File.expand_path(path, dir) unless path.start_with?("/")
|
|
192
|
-
return if File.file?(path) && File.read(path).lines.any? { |l| l.strip == MARKER_FILE }
|
|
193
|
-
|
|
194
261
|
FileUtils.mkdir_p(File.dirname(path))
|
|
195
|
-
File.
|
|
262
|
+
existing = File.file?(path) ? File.read(path).lines.map(&:strip) : []
|
|
263
|
+
additions = Array(names).reject { |n| existing.include?(n) }
|
|
264
|
+
return if additions.empty?
|
|
265
|
+
|
|
266
|
+
File.open(path, "a") { |f| additions.each { |n| f.puts n } }
|
|
196
267
|
rescue SystemCallError, IOError
|
|
197
268
|
nil
|
|
198
269
|
end
|
data/lib/yamine/probe.rb
CHANGED
|
@@ -65,7 +65,11 @@ module Yamine
|
|
|
65
65
|
url = "#{h[:adapter]}:#{db}"
|
|
66
66
|
end
|
|
67
67
|
end
|
|
68
|
-
{ "name" => c.name, "database" => db, "url" => url
|
|
68
|
+
{ "name" => c.name, "database" => db, "url" => url,
|
|
69
|
+
# What DATABASE_URL held INSIDE the app — the discriminator
|
|
70
|
+
# between "no .env loader ran" (nil) and "loader ran but
|
|
71
|
+
# database.yml's url: keys took precedence over it".
|
|
72
|
+
"env_database_url" => ENV["DATABASE_URL"] }
|
|
69
73
|
end
|
|
70
74
|
puts "YAMINE_DBS=#{JSON.generate(rows)}"
|
|
71
75
|
RUBY
|
data/lib/yamine/version.rb
CHANGED