yamine 0.11.0 → 0.12.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 +78 -0
- data/README.md +24 -4
- data/lib/ask/skills/yamine/SKILL.md +13 -3
- data/lib/yamine/cli/boot.rb +45 -15
- data/lib/yamine/cli/routes.rb +16 -3
- data/lib/yamine/resolver.rb +38 -9
- data/lib/yamine/runner.rb +15 -6
- data/lib/yamine/variant.rb +22 -5
- 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: 4a9a67fc55d6a6514571970b0a4ee40a682878c28b4a2bdf3a140a7a509715c8
|
|
4
|
+
data.tar.gz: e3332534d4123efbc4fea82c06c0af753866d4ca48db335860e921121baf2e12
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 7306ce065ecb6ff40ee10465f3fffc5108d6786a596c2783194459ec143e42649026f7b14ba170e3bbc9561d14738a613b0da8654ed59a335ba2e54e818a6345
|
|
7
|
+
data.tar.gz: fb1603c3b379c8067ce51a9aec0baf51a669aec3a9a74002493d9d750e7d224ebb9c8feeb46d786a84f1c0f035d590960e4943feb56e0fb9da7c3c8da1b1b186
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,83 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [0.12.0] — 2026-09-11
|
|
4
|
+
|
|
5
|
+
### Fixed
|
|
6
|
+
|
|
7
|
+
- **Worktrees get their own URL, as the README always claimed.**
|
|
8
|
+
`Variant.resolve`, the worktree detector, `Hostname.compose`'s variant
|
|
9
|
+
axis and their unit tests were all present, correct, and never
|
|
10
|
+
assembled: `Resolver.resolve` never called `Variant.resolve`, and
|
|
11
|
+
`hostname_for` built `{service}.{app}.{tld}` by hand, bypassing the
|
|
12
|
+
composer. So every worktree of a project answered on the *same*
|
|
13
|
+
hostname as its main checkout, and the ownership gate refused to boot
|
|
14
|
+
the second one — while telling the user to "work in your own worktree
|
|
15
|
+
(each branch gets its own URL)", advice that could not work.
|
|
16
|
+
|
|
17
|
+
A linked worktree is now reachable at `<branch>.<app>.<tld>`
|
|
18
|
+
(`work/next` → `work-next.myapp.localhost`) and boots beside its main
|
|
19
|
+
checkout, which keeps the bare name. Cross-checked by serving two
|
|
20
|
+
branches at once: distinct ports, distinct asset digests, no shared
|
|
21
|
+
route.
|
|
22
|
+
|
|
23
|
+
Found while trying to run a second checkout of a Rails app for UI work,
|
|
24
|
+
which is exactly the case the feature existed for.
|
|
25
|
+
|
|
26
|
+
### Changed
|
|
27
|
+
|
|
28
|
+
- **The variant axis is split in two: `overlay` and `variant`.** They
|
|
29
|
+
were one setting, which meant that as soon as a worktree supplied its
|
|
30
|
+
own variant, `config/local.<branch>.yml` became an implied file lookup
|
|
31
|
+
— so a branch named like a file on disk would silently change the
|
|
32
|
+
config. Now only an explicit `--variant` / `YAMINE_VARIANT` sets the
|
|
33
|
+
overlay to merge; a worktree branch supplies the hostname label and
|
|
34
|
+
nothing else. `status` prints both lines, so the two can no longer be
|
|
35
|
+
mistaken for each other.
|
|
36
|
+
|
|
37
|
+
- **A branch label is the whole branch, not its last path segment.**
|
|
38
|
+
`feature/login` and `bugfix/login` are different worktrees and used to
|
|
39
|
+
collapse to the same `login` hostname. Overlong branches still resolve
|
|
40
|
+
through the existing hash-suffix truncation.
|
|
41
|
+
|
|
42
|
+
- A worktree on a detached HEAD falls back to its directory name (the
|
|
43
|
+
same identity the per-worktree database uses) instead of returning no
|
|
44
|
+
prefix and quietly colliding with the main checkout.
|
|
45
|
+
|
|
46
|
+
- `yamine status` now honors `--variant` / `--tld`, and accepts
|
|
47
|
+
`--branch`, which the README documented but `parse_flags` rejected as
|
|
48
|
+
an unknown flag.
|
|
49
|
+
|
|
50
|
+
- `proxy.host` is never rewritten by a variant: an explicitly written
|
|
51
|
+
hostname is left exactly as written.
|
|
52
|
+
|
|
53
|
+
## [0.11.1] — 2026-09-11
|
|
54
|
+
|
|
55
|
+
### Fixed
|
|
56
|
+
|
|
57
|
+
- **`env:` in `config/local.yml` now actually reaches the processes.**
|
|
58
|
+
It never did. `build_env` was written, correct-looking, and had no
|
|
59
|
+
callers — so every top-level `env.clear` value and every `env.secret`
|
|
60
|
+
was parsed, validated, and dropped on the floor. It stayed invisible
|
|
61
|
+
because `DATABASE_URL` had its own path to the child; a config whose
|
|
62
|
+
*integration* depended on that block booted with the variable blank,
|
|
63
|
+
which looks exactly like a code bug.
|
|
64
|
+
|
|
65
|
+
Found while wiring a Rails app to a sibling service at
|
|
66
|
+
`https://other-app.localhost`: the URL was in `config/local.yml`, the
|
|
67
|
+
process had `nil`.
|
|
68
|
+
|
|
69
|
+
Both levels now merge, process over top-level, with `env.secret` names
|
|
70
|
+
resolved from `config/local.secrets` (or an existing export). A declared
|
|
71
|
+
secret nobody supplies warns at boot instead of starting blank — an app
|
|
72
|
+
with a missing API key fails somewhere far less obvious later.
|
|
73
|
+
|
|
74
|
+
yamine's own variables (`PORT`, `YAMINE_URL`, `DATABASE_URL`, the TLS
|
|
75
|
+
paths) are applied *after* the config, so a stray key in `local.yml`
|
|
76
|
+
cannot steer a process off its route. Pinned end to end by a test that
|
|
77
|
+
spawns a real process and reads the value out of its environment —
|
|
78
|
+
asserting on `build_env`'s return would have passed on the broken
|
|
79
|
+
version too.
|
|
80
|
+
|
|
3
81
|
## [0.11.0] — 2026-09-11
|
|
4
82
|
|
|
5
83
|
### Added
|
data/README.md
CHANGED
|
@@ -117,7 +117,8 @@ whole tree, and cleans up when one exits.
|
|
|
117
117
|
|
|
118
118
|
Variants are file overlays: `config/local.<variant>.yml` deep-merges on
|
|
119
119
|
top of `config/local.yml`, selected by `YAMINE_VARIANT` (Kamal's
|
|
120
|
-
destination pattern).
|
|
120
|
+
destination pattern). Naming a variant also prefixes the hostname — see
|
|
121
|
+
below for how that differs from a worktree's automatic prefix.
|
|
121
122
|
|
|
122
123
|
`.localhost` resolves to loopback natively in Chrome, Firefox, and Edge —
|
|
123
124
|
no DNS server, no `/etc/resolver`. Safari may need `yamine hosts sync`.
|
|
@@ -135,10 +136,24 @@ no DNS server, no `/etc/resolver`. Safari may need `yamine hosts sync`.
|
|
|
135
136
|
| variant | `fix-ui.myapp` | `--variant`, `YAMINE_VARIANT`, linked worktree branch |
|
|
136
137
|
| tld | `myapp.preview.example.com` | `--tld` (default `localhost`) |
|
|
137
138
|
|
|
139
|
+
`proxy.host` is the one exception: an explicitly written full hostname
|
|
140
|
+
bypasses composition entirely, variant included.
|
|
141
|
+
|
|
138
142
|
Linked git worktrees get a branch prefix automatically
|
|
139
|
-
(`
|
|
140
|
-
|
|
141
|
-
|
|
143
|
+
(`ui-onboarding.myapp.localhost`); the main checkout keeps the bare name,
|
|
144
|
+
so a worktree and its main checkout run side by side. The label is the
|
|
145
|
+
whole branch (`feature/login` → `feature-login.myapp.localhost`), so two
|
|
146
|
+
branches that share a last segment never share a hostname. A detached
|
|
147
|
+
HEAD has no branch to name it and falls back to the worktree's directory
|
|
148
|
+
— the same identity its per-worktree database uses. `main`/`master`
|
|
149
|
+
never prefix. Pass `--branch` (or `YAMINE_BRANCH=1`) to prefix by the
|
|
150
|
+
current branch outside worktrees.
|
|
151
|
+
|
|
152
|
+
A variant is a hostname label, not a config file. Only an explicit
|
|
153
|
+
`--variant` / `YAMINE_VARIANT` goes looking for
|
|
154
|
+
`config/local.<name>.yml` to merge; a worktree branch never does, so a
|
|
155
|
+
branch named like a file on disk cannot change your config by accident.
|
|
156
|
+
`yamine status` prints both lines so the two are never confused.
|
|
142
157
|
|
|
143
158
|
```bash
|
|
144
159
|
yamine # -> https://myapp.localhost
|
|
@@ -147,6 +162,11 @@ yamine --variant demo # -> https://demo.myapp.localhos
|
|
|
147
162
|
yamine --tld preview.example.com # your own domain (OAuth parity)
|
|
148
163
|
```
|
|
149
164
|
|
|
165
|
+
Because a registered route answers `*.` subdomains of itself, any
|
|
166
|
+
`<label>.myapp.localhost` reaches the main checkout until a worktree
|
|
167
|
+
registers that exact name — at which point the exact route wins and the
|
|
168
|
+
worktree takes over.
|
|
169
|
+
|
|
150
170
|
## Commands
|
|
151
171
|
|
|
152
172
|
```bash
|
|
@@ -78,16 +78,26 @@ yamine get backend --variant demo
|
|
|
78
78
|
Use `get` output for frontend-to-API URLs, Cable URLs, and webhook
|
|
79
79
|
targets. Do not guess ports.
|
|
80
80
|
|
|
81
|
-
## Variants are files
|
|
81
|
+
## Variants are files; worktrees are not
|
|
82
82
|
|
|
83
83
|
A variant is a file overlay, Kamal-style: `config/local.<variant>.yml`
|
|
84
|
-
deep-merges over `config/local.yml
|
|
85
|
-
|
|
84
|
+
deep-merges over `config/local.yml`, selected with `YAMINE_VARIANT` or
|
|
85
|
+
`--variant`. Naming one also prefixes the hostname.
|
|
86
|
+
|
|
87
|
+
A git worktree needs none of that: it gets a branch prefix automatically
|
|
88
|
+
(`ui-onboarding.myapp.localhost`) and boots alongside its main checkout,
|
|
89
|
+
which keeps the bare name. The whole branch is the label, so
|
|
90
|
+
`feature/login` and `bugfix/login` stay distinct. A detached worktree
|
|
91
|
+
falls back to its directory name.
|
|
86
92
|
|
|
87
93
|
```bash
|
|
88
94
|
YAMINE_VARIANT=fix-ui yamine # boots with config/local.fix-ui.yml merged
|
|
89
95
|
```
|
|
90
96
|
|
|
97
|
+
Only an explicit variant ever looks for an overlay file — a branch name
|
|
98
|
+
that happens to match one on disk is ignored. `yamine status` shows
|
|
99
|
+
`variant:` and `overlay:` separately.
|
|
100
|
+
|
|
91
101
|
## First time on a machine
|
|
92
102
|
|
|
93
103
|
Run `yamine start` — it does the one-shot CA trust, port 443, and
|
data/lib/yamine/cli/boot.rb
CHANGED
|
@@ -16,8 +16,9 @@ module Yamine
|
|
|
16
16
|
# boots every process, supervises the tree, cleans up on exit.
|
|
17
17
|
def run_inferred(ctx, args)
|
|
18
18
|
variant = ENV["YAMINE_VARIANT"]
|
|
19
|
-
opts = ctx.parse_flags(args, %i[variant tld force app_port wait no_wait json])
|
|
20
|
-
resolved = resolve!(ctx, variant: opts[:variant] || variant, tld: opts[:tld]
|
|
19
|
+
opts = ctx.parse_flags(args, %i[variant tld force app_port wait no_wait json branch])
|
|
20
|
+
resolved = resolve!(ctx, variant: opts[:variant] || variant, tld: opts[:tld],
|
|
21
|
+
use_branch: opts[:branch])
|
|
21
22
|
# Ownership gate before any side effects: no proxy spawn, no
|
|
22
23
|
# port allocation when we'd refuse anyway.
|
|
23
24
|
check_worktree_ownership!(ctx, resolved, force: opts[:force])
|
|
@@ -241,7 +242,8 @@ module Yamine
|
|
|
241
242
|
app = runner.boot_run(name: item[:name], hostname: item[:hostname],
|
|
242
243
|
url: item[:url], dir: Dir.pwd, command: item[:command],
|
|
243
244
|
port: item[:port], force: opts[:force],
|
|
244
|
-
rails_dev_host: item[:hostname], database_url: db_url
|
|
245
|
+
rails_dev_host: item[:hostname], database_url: db_url,
|
|
246
|
+
extra_env: build_env(resolved, item[:entry], proc_name: item[:name]))
|
|
245
247
|
routes_registered << { hostnames: item[:hostnames], app: app }
|
|
246
248
|
|
|
247
249
|
say opts, " -> #{item[:url]}"
|
|
@@ -261,7 +263,8 @@ module Yamine
|
|
|
261
263
|
app = runner.spawn_http(name: item[:name], hostname: item[:hostname],
|
|
262
264
|
url: item[:url], dir: Dir.pwd, command: item[:command],
|
|
263
265
|
port: item[:port], rails_dev_host: item[:hostname],
|
|
264
|
-
database_url: db_url, force: opts[:force]
|
|
266
|
+
database_url: db_url, force: opts[:force],
|
|
267
|
+
extra_env: build_env(resolved, item[:entry], proc_name: item[:name]))
|
|
265
268
|
# Tracked before registration so any later failure (a raise
|
|
266
269
|
# during adopt, a crash while another process waits) can
|
|
267
270
|
# always reap it. Duplicate names in named_pids are harmless.
|
|
@@ -359,7 +362,8 @@ module Yamine
|
|
|
359
362
|
app = runner.boot_run(name: proc_name, hostname: "#{resolved.app}.#{proc_name}.internal",
|
|
360
363
|
url: url, dir: Dir.pwd, command: ["sh", "-c", cmd], port: port,
|
|
361
364
|
force: opts[:force], rails_dev_host: nil, register: false,
|
|
362
|
-
database_url: db_url
|
|
365
|
+
database_url: db_url,
|
|
366
|
+
extra_env: build_env(resolved, entry, proc_name: proc_name))
|
|
363
367
|
children << { name: proc_name, pid: app.pid }
|
|
364
368
|
say opts, " [#{proc_name}] background (pid #{app.pid})"
|
|
365
369
|
end
|
|
@@ -398,18 +402,44 @@ module Yamine
|
|
|
398
402
|
end
|
|
399
403
|
end
|
|
400
404
|
|
|
401
|
-
|
|
405
|
+
# The environment a process gets, from the config.
|
|
406
|
+
#
|
|
407
|
+
# Two levels, and the merge order is the point: top-level `env:`
|
|
408
|
+
# describes the whole app (an API URL every process needs), a
|
|
409
|
+
# process's own `env:` describes that process, and the process wins
|
|
410
|
+
# where they collide — "this one worker talks to staging" should not
|
|
411
|
+
# require restructuring the file.
|
|
412
|
+
#
|
|
413
|
+
# `env.clear` is literal values; `env.secret` names keys whose values
|
|
414
|
+
# come from config/local.secrets (dotenv, gitignored), so a credential
|
|
415
|
+
# stays out of the config file while still reaching the process. A
|
|
416
|
+
# named secret that is missing is reported rather than silently
|
|
417
|
+
# dropped: an app booting with a blank API key fails later, somewhere
|
|
418
|
+
# far less obvious.
|
|
419
|
+
def build_env(resolved, entry, proc_name: nil)
|
|
402
420
|
env = {}
|
|
403
|
-
|
|
404
|
-
|
|
421
|
+
|
|
422
|
+
top = resolved.env.is_a?(Hash) ? resolved.env : {}
|
|
423
|
+
(top["clear"] || {}).each { |k, v| env[k] = v }
|
|
424
|
+
|
|
405
425
|
entry_env = entry["env"] || {}
|
|
406
426
|
(entry_env["clear"] || {}).each { |k, v| env[k] = v }
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
427
|
+
|
|
428
|
+
secrets = resolved.secrets || {}
|
|
429
|
+
(Array(top["secret"]) + Array(entry_env["secret"])).uniq.each do |key|
|
|
430
|
+
if secrets.key?(key)
|
|
431
|
+
env[key] = secrets[key]
|
|
432
|
+
elsif ENV.key?(key)
|
|
433
|
+
# Already in the environment (a shell export, an agent's
|
|
434
|
+
# session) — the declaration is satisfied.
|
|
435
|
+
env[key] = ENV[key]
|
|
436
|
+
else
|
|
437
|
+
label = proc_name ? "#{proc_name}: " : ""
|
|
438
|
+
$stderr.puts " #{label}warning: env.secret lists #{key}, but it is not in " \
|
|
439
|
+
"config/local.secrets (and not in the environment) — starting without it"
|
|
440
|
+
end
|
|
411
441
|
end
|
|
412
|
-
|
|
442
|
+
|
|
413
443
|
env
|
|
414
444
|
end
|
|
415
445
|
|
|
@@ -624,10 +654,10 @@ module Yamine
|
|
|
624
654
|
end
|
|
625
655
|
end
|
|
626
656
|
|
|
627
|
-
def resolve!(ctx, variant: nil, tld: nil)
|
|
657
|
+
def resolve!(ctx, variant: nil, tld: nil, use_branch: false)
|
|
628
658
|
# The resolver calls Config.load, which raises ConfigError if
|
|
629
659
|
# config/local.yml is missing — exactly what we want.
|
|
630
|
-
Yamine::Resolver.resolve(Dir.pwd, variant: variant, tld: tld)
|
|
660
|
+
Yamine::Resolver.resolve(Dir.pwd, variant: variant, tld: tld, use_branch: use_branch)
|
|
631
661
|
end
|
|
632
662
|
|
|
633
663
|
# json: keeps stdout free for the machine-readable stream — this runs
|
data/lib/yamine/cli/routes.rb
CHANGED
|
@@ -175,6 +175,9 @@ module Yamine
|
|
|
175
175
|
# agent's cleanup). --force overrides and names the previous owner.
|
|
176
176
|
def stop(ctx, args, out: $stdout)
|
|
177
177
|
force = args.delete("--force")
|
|
178
|
+
# In a worktree this resolves that worktree's own hostname (the
|
|
179
|
+
# branch is the variant), so `stop` here never reaches across
|
|
180
|
+
# into the main checkout's running app.
|
|
178
181
|
resolved = Resolver.resolve(Dir.pwd)
|
|
179
182
|
hostnames = Resolver.hostnames(resolved)
|
|
180
183
|
mine = Agent.name
|
|
@@ -261,9 +264,14 @@ module Yamine
|
|
|
261
264
|
# `yamine` would boot here and why. Answers "why did I get
|
|
262
265
|
# this URL" without booting anything. --json emits stable keys
|
|
263
266
|
# for agents instead of prose.
|
|
264
|
-
def status(
|
|
267
|
+
def status(ctx, args)
|
|
265
268
|
json = args.delete("--json")
|
|
266
|
-
|
|
269
|
+
# The flags exist so `status --variant x` answers the same
|
|
270
|
+
# question boot would; in a worktree the branch supplies the
|
|
271
|
+
# variant anyway, so plain `status` is already the right answer.
|
|
272
|
+
opts = ctx.parse_flags(args, %i[variant tld branch])
|
|
273
|
+
resolved = Resolver.resolve(Dir.pwd, variant: opts[:variant], tld: opts[:tld],
|
|
274
|
+
use_branch: opts[:branch])
|
|
267
275
|
hostnames = Resolver.hostnames(resolved)
|
|
268
276
|
urls = hostnames.map do |h|
|
|
269
277
|
Hostname.url(h, port: ProxyControl.default_port(true), tls: true)
|
|
@@ -273,6 +281,7 @@ module Yamine
|
|
|
273
281
|
tld: resolved.tld, tld_source: resolved.sources[:tld],
|
|
274
282
|
host: resolved.host, host_source: resolved.sources[:host],
|
|
275
283
|
variant: resolved.variant, variant_source: resolved.sources[:variant],
|
|
284
|
+
overlay: resolved.overlay, overlay_source: resolved.sources[:overlay],
|
|
276
285
|
urls: urls,
|
|
277
286
|
processes: resolved.processes.keys,
|
|
278
287
|
framework: Framework.detect(Dir.pwd).to_s
|
|
@@ -288,7 +297,11 @@ module Yamine
|
|
|
288
297
|
else
|
|
289
298
|
puts "tld: #{payload[:tld]} (from #{payload[:tld_source]})"
|
|
290
299
|
end
|
|
291
|
-
puts "variant: #{payload[:variant] || "(none)"} (from #{payload[:variant_source] || "no
|
|
300
|
+
puts "variant: #{payload[:variant] || "(none)"} (from #{payload[:variant_source] || "no flag, env, or worktree branch"})"
|
|
301
|
+
# The overlay is a different thing from the variant and only an
|
|
302
|
+
# explicit --variant / YAMINE_VARIANT ever sets it, so say so
|
|
303
|
+
# rather than letting the two look like one setting.
|
|
304
|
+
puts "overlay: #{payload[:overlay] ? "#{payload[:overlay]} (merged)" : "(none)"}"
|
|
292
305
|
puts "processes: #{payload[:processes].join(", ")}"
|
|
293
306
|
puts "urls:"
|
|
294
307
|
urls.each { |u| puts " #{u}" }
|
data/lib/yamine/resolver.rb
CHANGED
|
@@ -9,10 +9,26 @@ module Yamine
|
|
|
9
9
|
module_function
|
|
10
10
|
|
|
11
11
|
Result = Struct.new(:app, :tld, :host, :processes, :secrets,
|
|
12
|
-
:sources, :variant, :db, :env, keyword_init: true)
|
|
12
|
+
:sources, :variant, :overlay, :db, :env, keyword_init: true)
|
|
13
13
|
|
|
14
|
-
|
|
15
|
-
|
|
14
|
+
# Two axes, and keeping them apart is the whole point:
|
|
15
|
+
#
|
|
16
|
+
# overlay — an explicit `config/local.<name>.yml` to deep-merge.
|
|
17
|
+
# Only --variant / YAMINE_VARIANT ever set it, so a
|
|
18
|
+
# branch name can never select config by surprise.
|
|
19
|
+
# variant — the leading hostname label, from that same explicit
|
|
20
|
+
# name OR a linked worktree's branch. It is a name, not
|
|
21
|
+
# a file: a worktree is reachable at
|
|
22
|
+
# <branch>.<app>.localhost without inventing config.
|
|
23
|
+
#
|
|
24
|
+
# They share a value when the user asks for one explicitly. Only
|
|
25
|
+
# then does a variant mean both "merge this file" and "prefix this
|
|
26
|
+
# hostname", which is the documented `--variant` behavior.
|
|
27
|
+
def resolve(dir = Dir.pwd, variant: nil, tld: nil, host: nil, use_branch: false)
|
|
28
|
+
overlay = (variant || ENV["YAMINE_VARIANT"])&.strip
|
|
29
|
+
overlay = nil if overlay&.empty?
|
|
30
|
+
|
|
31
|
+
config = Config.load(dir, variant: overlay)
|
|
16
32
|
unless config
|
|
17
33
|
raise ConfigError, Config.missing_message(dir)
|
|
18
34
|
end
|
|
@@ -21,8 +37,7 @@ module Yamine
|
|
|
21
37
|
proxy = config.proxy_config
|
|
22
38
|
processes = config.processes
|
|
23
39
|
|
|
24
|
-
variant_name = (variant
|
|
25
|
-
variant_name = nil if variant_name&.empty?
|
|
40
|
+
variant_name, variant_source = Variant.resolve(dir, explicit: variant, use_branch: use_branch)
|
|
26
41
|
|
|
27
42
|
# host or tld: config proxy.host wins over proxy.tld; flags override.
|
|
28
43
|
if host || proxy["host"]
|
|
@@ -41,7 +56,8 @@ module Yamine
|
|
|
41
56
|
app: config.path.to_s,
|
|
42
57
|
tld: proxy["tld"] ? "#{config.path} proxy.tld" : "default (localhost)",
|
|
43
58
|
host: proxy["host"] ? "#{config.path} proxy.host" : nil,
|
|
44
|
-
variant:
|
|
59
|
+
variant: variant_source,
|
|
60
|
+
overlay: overlay ? "config/local.#{overlay}.yml" : nil
|
|
45
61
|
}
|
|
46
62
|
|
|
47
63
|
Result.new(
|
|
@@ -52,20 +68,33 @@ module Yamine
|
|
|
52
68
|
secrets: config.secrets,
|
|
53
69
|
sources: sources,
|
|
54
70
|
variant: variant_name,
|
|
71
|
+
overlay: overlay,
|
|
55
72
|
db: config.data["db"],
|
|
56
73
|
env: config.env_config
|
|
57
74
|
)
|
|
58
75
|
end
|
|
59
76
|
|
|
60
77
|
# Hostname for one process: primary (first proxy:true) is bare;
|
|
61
|
-
# others are proc.app.tld; proxy:false have none.
|
|
78
|
+
# others are proc.app.tld; proxy:false have none. A variant — the
|
|
79
|
+
# explicit name or a worktree's branch — leads every one of them.
|
|
62
80
|
def hostname_for(result, proc_name)
|
|
63
81
|
entry = result.processes[proc_name]
|
|
64
82
|
return nil unless entry
|
|
65
83
|
return nil if entry["proxy"] == false
|
|
66
84
|
|
|
67
|
-
|
|
68
|
-
|
|
85
|
+
# An explicit proxy.host is a full hostname, so composition does
|
|
86
|
+
# not apply — and a variant must never silently rewrite a host
|
|
87
|
+
# the user wrote out in full.
|
|
88
|
+
if result.host
|
|
89
|
+
return proc_name.to_s == primary_proc(result) ? result.host : "#{proc_name}.#{result.host}"
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
Hostname.compose(
|
|
93
|
+
app: result.app,
|
|
94
|
+
tld: result.tld,
|
|
95
|
+
service: (proc_name.to_s == primary_proc(result) ? nil : proc_name.to_s),
|
|
96
|
+
variant: result.variant
|
|
97
|
+
)
|
|
69
98
|
end
|
|
70
99
|
|
|
71
100
|
def primary_proc(result)
|
data/lib/yamine/runner.rb
CHANGED
|
@@ -93,11 +93,16 @@ module Yamine
|
|
|
93
93
|
# register: false spawns without a route (background processes).
|
|
94
94
|
# Child output goes to log/yamine-<name>.log so `--wait` failure
|
|
95
95
|
# payloads can tail it (same file the managed path uses).
|
|
96
|
+
#
|
|
97
|
+
# extra_env is the config's env.clear/env.secret for this process —
|
|
98
|
+
# yamine's own keys (PORT, YAMINE_URL, DATABASE_URL, the TLS vars)
|
|
99
|
+
# win over it, because those describe the boot rather than the app.
|
|
96
100
|
def boot_run(name:, hostname:, url:, dir:, command:, port: nil, force: false,
|
|
97
|
-
rails_dev_host: nil, register: true, database_url: nil, spec: nil
|
|
101
|
+
rails_dev_host: nil, register: true, database_url: nil, spec: nil,
|
|
102
|
+
extra_env: nil)
|
|
98
103
|
port ||= Ports.find_free
|
|
99
104
|
env = child_env(dir, url: url, port: port, rails_dev_host: rails_dev_host,
|
|
100
|
-
database_url: database_url)
|
|
105
|
+
database_url: database_url, extra_env: extra_env)
|
|
101
106
|
path = log_path(dir, name)
|
|
102
107
|
pid = with_clean_env { spawn(env, *command, chdir: dir, out: path, err: [:child, :out]) }
|
|
103
108
|
Process.detach(pid)
|
|
@@ -124,9 +129,9 @@ module Yamine
|
|
|
124
129
|
# Returns the placeholder App (target known before bind). Fate of
|
|
125
130
|
# the backend is decided by wait, not by spawn.
|
|
126
131
|
def spawn_http(name:, hostname:, url:, dir:, command:, port:, rails_dev_host: nil,
|
|
127
|
-
database_url: nil, force: false)
|
|
132
|
+
database_url: nil, force: false, extra_env: nil)
|
|
128
133
|
env = child_env(dir, url: url, port: port, rails_dev_host: rails_dev_host,
|
|
129
|
-
database_url: database_url)
|
|
134
|
+
database_url: database_url, extra_env: extra_env)
|
|
130
135
|
path = log_path(dir, name)
|
|
131
136
|
pid = with_clean_env { spawn(env, *command, chdir: dir, out: path, err: [:child, :out]) }
|
|
132
137
|
Process.detach(pid)
|
|
@@ -151,8 +156,12 @@ module Yamine
|
|
|
151
156
|
false
|
|
152
157
|
end
|
|
153
158
|
|
|
154
|
-
def child_env(dir, url:, port:, rails_dev_host: nil, database_url: nil)
|
|
155
|
-
|
|
159
|
+
def child_env(dir, url:, port:, rails_dev_host: nil, database_url: nil, extra_env: nil)
|
|
160
|
+
# The config's own variables first, so everything below overrides
|
|
161
|
+
# them: PORT, YAMINE_URL, DATABASE_URL, and the TLS paths describe
|
|
162
|
+
# *this boot* and must not be lost to a stray key in local.yml.
|
|
163
|
+
env = (extra_env || {}).transform_keys(&:to_s).transform_values(&:to_s)
|
|
164
|
+
env["YAMINE_URL"] = url
|
|
156
165
|
env["PORT"] = port.to_s if port
|
|
157
166
|
env["HOST"] = "127.0.0.1"
|
|
158
167
|
|
data/lib/yamine/variant.rb
CHANGED
|
@@ -51,12 +51,27 @@ module Yamine
|
|
|
51
51
|
label.empty? ? nil : [label, source]
|
|
52
52
|
end
|
|
53
53
|
|
|
54
|
+
# The whole branch, not just its last path segment. `feature/login`
|
|
55
|
+
# and `bugfix/login` are different worktrees and must not end up
|
|
56
|
+
# sharing a hostname; taking the last segment alone collapsed them
|
|
57
|
+
# both to `login`.
|
|
54
58
|
def branch_to_prefix(branch)
|
|
55
59
|
return nil if branch.nil? || branch.empty?
|
|
56
60
|
return nil if branch == "HEAD" || DEFAULT_BRANCHES.include?(branch)
|
|
57
61
|
|
|
58
|
-
|
|
59
|
-
label
|
|
62
|
+
label = Sanitize.hostname_label(branch)
|
|
63
|
+
label.empty? ? nil : label
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
# A detached HEAD has no branch to name it, so the worktree's
|
|
67
|
+
# directory does — the same identity the per-worktree database is
|
|
68
|
+
# keyed on. Without this, a detached worktree would fall back to the
|
|
69
|
+
# bare app name and answer on the main checkout's hostname.
|
|
70
|
+
def worktree_dir_prefix(cwd)
|
|
71
|
+
top, status = git(cwd, "rev-parse", "--show-toplevel")
|
|
72
|
+
return nil unless status.success?
|
|
73
|
+
|
|
74
|
+
label = Sanitize.hostname_label(File.basename(top.strip))
|
|
60
75
|
label.empty? ? nil : label
|
|
61
76
|
end
|
|
62
77
|
|
|
@@ -81,7 +96,7 @@ module Yamine
|
|
|
81
96
|
branch, s3 = git(cwd, "rev-parse", "--abbrev-ref", "HEAD")
|
|
82
97
|
return nil unless s3.success?
|
|
83
98
|
|
|
84
|
-
branch_to_prefix(branch.strip)
|
|
99
|
+
branch_to_prefix(branch.strip) || worktree_dir_prefix(cwd)
|
|
85
100
|
rescue SystemCallError
|
|
86
101
|
filesystem_worktree_prefix(cwd)
|
|
87
102
|
end
|
|
@@ -98,8 +113,10 @@ module Yamine
|
|
|
98
113
|
if match && match[1].match?(%r{[/\\]worktrees[/\\][^/\\]+\z})
|
|
99
114
|
head = File.join(File.expand_path(match[1], dir.to_s), "HEAD")
|
|
100
115
|
branch = read_branch_from_head(head)
|
|
101
|
-
|
|
102
|
-
|
|
116
|
+
# Same rule as the git path: branch when there is one, the
|
|
117
|
+
# worktree directory otherwise (detached HEAD).
|
|
118
|
+
prefix = branch_to_prefix(branch.to_s) || Sanitize.hostname_label(dir.basename.to_s)
|
|
119
|
+
return prefix.empty? ? nil : prefix
|
|
103
120
|
end
|
|
104
121
|
return nil
|
|
105
122
|
end
|
data/lib/yamine/version.rb
CHANGED