yamine 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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 3987eef1f0bc39291b8d264fd4b569fcb54106c23e78150594f62d86ca2d0351
4
- data.tar.gz: e98404e9655e78861c289cb320bf544564e07e65e06fe32a496d5a3ff4ee52b9
3
+ metadata.gz: 43b8c059b0635ce4272b806fd2b8f70ae42ed6d591ed87536de9c86ed0419864
4
+ data.tar.gz: c34fb3a40d5c16442accee4dcbdb32b1b2b5013de5c3a823bade745b6ac65e88
5
5
  SHA512:
6
- metadata.gz: 20018aea0d2229ab0c6c814d15deaeb176649fcdaa81937eb2c6f223607e50f2c34256ed078aca6ef0bb0a114cbd865bd6187dd2f74a1b981d2dbd7c8d7e141d
7
- data.tar.gz: 808f2b4151de99fa3fe71ba8bbbfce6eb7dbe63298bea1da3c4fd05a5baea9da7f5d797ae792057f8ddd2f5500b19da47fd97af1641481764eb8f051605a0c0f
6
+ metadata.gz: 70495a26f226c3b0dac853c5a4ce1694207f4a9bbb73af92b44b37cc17ff3c13430af7440252102b8acd40f22ee7efdae5dfec26155678a5839e30b10c5c0d1a
7
+ data.tar.gz: e450abc81d00dad86bf03e540304ad43683e99329a60eb24ddea0122760048c88729e7fe33c9b93056a1840ec871e488ea458d1afdbd655b0dccd477369b8cb5
data/CHANGELOG.md CHANGED
@@ -2,6 +2,28 @@
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
+
5
27
  ## [0.17.0] — 2026-09-23
6
28
 
7
29
  ### Added
data/README.md CHANGED
@@ -253,13 +253,20 @@ yamine db create # re-probe + provision (run after the app grows a da
253
253
  (any dotenv loader works). That is what reads the files
254
254
  `worktree add` writes into the worktree:
255
255
 
256
- - `.env` / `.env.development` — the whole development set
257
- (`DATABASE_URL` plus one `NAME_DATABASE_URL` per configuration),
256
+ - `.env.development` — the whole development set (`DATABASE_URL`
257
+ plus one `NAME_DATABASE_URL` per configuration),
258
258
  - `.env.test` — the test URL under `PRIMARY_DATABASE_URL`, the key
259
259
  Rails checks *before* `DATABASE_URL` for a flat test config, so it
260
260
  wins regardless of the order a loader reads the files in.
261
261
 
262
- Mode 0600, git-excluded automatically, removed with the worktree.
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`.
263
270
  A hand-run `rails console`, `rails test`, or `db:migrate` in the
264
271
  worktree therefore lands on the worktree's own databases — with
265
272
  **zero yamine-specific code in `database.yml`, ever**.
@@ -275,7 +282,7 @@ Credential keys travel too: `config/master.key`,
275
282
  `config/credentials/test.key` are copied at mode 0600. Production and
276
283
  staging keys stay in the main checkout where they belong.
277
284
 
278
- The main checkout never gets `.env` files or a suffix: its databases
285
+ The main checkout never gets these env files or a suffix: its databases
279
286
  are its databases, untouched.
280
287
 
281
288
 
@@ -116,9 +116,11 @@ credentials resolve inside the app; yamine never parses them), and
116
116
  provisions the whole set with schema — every database of a
117
117
  multi-database app gets a per-worktree suffix, the test database
118
118
  included and schema-prepared, so `rails test` runs as-is. It writes
119
- `.env` / `.env.development` (the development set) and `.env.test`
119
+ `.env.development` (the development set) and `.env.test`
120
120
  (`PRIMARY_DATABASE_URL` — the key Rails checks first for a flat test
121
- config) into the worktree, git-excluded automatically. Then boot with
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
122
124
  `yamine start` inside it. `remove` and `clean` drop the entire set as
123
125
  a unit — including from an orphaned claim whose directory is gone,
124
126
  without booting the app — and `clean` never touches uncommitted work;
@@ -133,7 +135,7 @@ worktree after the app grows a database).
133
135
 
134
136
  Isolation reaches two places: supervised boots via injected
135
137
  `DATABASE_URL` / `NAME_DATABASE_URL` env vars, and hand-run commands
136
- (`rails console`, `rails test`, `db:migrate`) via the `.env` files —
138
+ (`rails console`, `rails test`, `db:migrate`) via these env files —
137
139
  which require a dotenv loader in the app (`gem "dotenv-rails",
138
140
  groups: [:development, :test]`) and **component-form development/test
139
141
  config** (`database:` keys, never `url:` — a `url:` key takes
@@ -32,17 +32,29 @@ module Yamine
32
32
 
33
33
  MAX_IDENTIFIER_BYTES = 63
34
34
  STATE_FILE = "databases.json"
35
- # The per-worktree environment files yamine writes: `.env` carries
36
- # the development set for ANY dotenv-style loader (framework-agnostic),
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
+ #
37
45
  # `.env.test` carries PRIMARY_DATABASE_URL — the key Rails checks
38
- # *before* DATABASE_URL for a flat test config, so the test URL wins
39
- # no matter which order a loader reads the two files in. Hand-run
40
- # commands (`rails console`, `rails test`, `db:migrate`) pick these
41
- # up; env injection still covers processes yamine spawns itself.
42
- ENV_FILES = %w[.env .env.development .env.test].freeze
43
- # 0.16.0's marker file — removed in favor of .env; deleted on sight
44
- # so upgraded worktrees don't accumulate dead files.
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.
45
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"
46
58
 
47
59
  # Database name for a worktree dir + env (development/test).
48
60
  # Main checkout (no worktree marker) keeps the bare name.
@@ -176,44 +188,63 @@ module Yamine
176
188
  name
177
189
  end
178
190
 
179
- # Write the worktree's environment files: `.env` with the whole
180
- # development set (dotenv-compatible — any framework or plain Ruby
181
- # app that loads .env gets hand-run isolation for free) and
182
- # `.env.test` with the test URL under PRIMARY_DATABASE_URL.
183
- # Private (URLs carry whatever the app's config carries), and the
184
- # 0.16.0 marker file is removed on the way past.
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.
185
198
  def write_env_files(dir, dev_env, test_url: nil)
186
199
  FileUtils.rm_f(File.join(dir, LEGACY_MARKER_FILE))
200
+ remove_legacy_worktree_env(dir)
187
201
  return if dev_env.nil? && test_url.nil?
188
202
 
189
203
  unless dev_env.nil? || dev_env.empty?
190
- body = dotenv_body(dev_env.reject { |k, _v| k == "PRIMARY_DATABASE_URL" })
191
- write_env_file(File.join(dir, ".env"), body)
192
- # Same content under the env-specific name: a loader that
193
- # prefers .env.development over .env finds identical values.
194
- write_env_file(File.join(dir, ".env.development"), body)
204
+ merge_env_file(File.join(dir, ".env.development"),
205
+ dev_env.reject { |k, _v| k == "PRIMARY_DATABASE_URL" })
195
206
  end
196
207
  return if test_url.nil? || test_url.to_s.empty?
197
208
 
198
- write_env_file(File.join(dir, ".env.test"),
199
- dotenv_body({ "PRIMARY_DATABASE_URL" => test_url }))
209
+ merge_env_file(File.join(dir, ".env.test"),
210
+ { "PRIMARY_DATABASE_URL" => test_url })
200
211
  end
201
212
 
202
- def write_env_file(path, body)
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
203
235
  File.write(path, body)
204
236
  File.chmod(0o600, path)
205
237
  end
206
238
 
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"
243
+ end
244
+
207
245
  # dotenv format: single-quoted values (literal — no interpolation
208
246
  # of a password's `$` or `#`), with the two escapes the dotenv
209
247
  # grammar allows inside them.
210
- def dotenv_body(env)
211
- header = "# Per-worktree databases, written by yamine (worktree add).\n" \
212
- "# Loaded by dotenv-rails / any dotenv loader; removed with this worktree.\n"
213
- lines = env.map { |k, v| "#{k}='#{dotenv_escape(v.to_s)}'" }
214
- "#{header}#{lines.join("\n")}\n"
215
- end
216
-
217
248
  def dotenv_escape(value)
218
249
  value.gsub("\\") { "\\\\" }.gsub("'") { "\\'" }
219
250
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Yamine
4
- VERSION = "0.17.0"
4
+ VERSION = "0.18.0"
5
5
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: yamine
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.17.0
4
+ version: 0.18.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Kaka Ruto