@simon-says-build/harbormaster 0.14.0 → 0.14.2

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.
Files changed (3) hide show
  1. package/README.md +19 -0
  2. package/harbormaster +32 -8
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -155,6 +155,25 @@ env, under the caller's explicit `--fn-env`. `exec`: on top of the caller's
155
155
  ambient env, so a key exported in some shell can't leak into the suite.
156
156
  `python3 test/project_env.py`.
157
157
 
158
+ ## A function env value can name a leased port (0.14.2)
159
+
160
+ `--fn-env KEY={port:firestore}`, or the same in `harbormaster.env`, becomes
161
+ this lease's firestore port, inside a value too (`127.0.0.1:{port:auth}`). A
162
+ caller can't know its ports before the lease, and an app whose functions read
163
+ their own variable for the emulator (Tapuat's `EMULATOR_PORT_FIRESTORE ||
164
+ "8080"`) otherwise talks to whatever holds the default. An unknown service is
165
+ left as written. `python3 test/port_env.py`.
166
+
167
+ ## A lease never reads the hosting config it doesn't start (0.14.1)
168
+
169
+ A lease whose `--only` leaves hosting out boots from a config with no
170
+ `hosting` block. firebase-tools 15.33 reads `hosting` even when it starts no
171
+ hosting emulator, and a web-framework target (`"source": "."`, Next.js)
172
+ makes it refuse every emulator without the `webframeworks` experiment.
173
+ Tapuat's lease on simon-linux-1 died there on 2026-10-21; 15.28 on the
174
+ studio never checked. An `--only` that asks for hosting, or no `--only`,
175
+ keeps it. `python3 test/injected_config.py`.
176
+
158
177
  ## An idle simulator nobody leases is shut down (0.13.0)
159
178
 
160
179
  The world sweep (every `FBPORTS_SWEEP_INTERVAL`, 5 min) now shuts down a
package/harbormaster CHANGED
@@ -84,7 +84,7 @@ import uuid
84
84
  from datetime import datetime, timezone
85
85
  from pathlib import Path
86
86
 
87
- VERSION = "0.14.0"
87
+ VERSION = "0.14.2"
88
88
 
89
89
  # Overridable for tests / parallel installs; default is the shared user daemon.
90
90
  # The dir keeps the old "firebase-ports" name: pre-rename binaries still installed
@@ -361,11 +361,35 @@ def _service_ports(ports):
361
361
  return {s: p for s, p in ports.items() if s not in ("ui", "hub", "logging")}
362
362
 
363
363
 
364
- def _write_injected_config(cwd, ports):
364
+ # A value that names a LEASED port: `{port:firestore}` becomes this lease's
365
+ # firestore port. The caller cannot know its ports before the lease, and an
366
+ # app whose functions read their own variable for the emulator (Tapuat's
367
+ # admin.ts: `127.0.0.1:${EMULATOR_PORT_FIRESTORE || "8080"}`, unconditionally)
368
+ # otherwise talks to whatever holds the default — on simon-linux-1, nothing;
369
+ # every callable 500'd (run 20261021-131438-exploration-everyday-user). An
370
+ # unknown service is left as written, so a typo shows in the app's own error.
371
+ _PORT_REF = re.compile(r"\{port:([a-z_]+)\}")
372
+
373
+ def _with_ports(env, ports):
374
+ return {k: _PORT_REF.sub(lambda m: str(ports[m.group(1)]) if m.group(1) in ports else m.group(0), v)
375
+ for k, v in env.items()}
376
+
377
+
378
+ def _write_injected_config(cwd, ports, only=None):
365
379
  """Set ports on the emulators the portless firebase.json DECLARES (+ hub/logging).
366
- Never adds emulators the project doesn't use. Returns the temp config basename."""
380
+ Never adds emulators the project doesn't use. Returns the temp config basename.
381
+
382
+ With an --only that leaves hosting out, the `hosting` block goes too: no
383
+ emulator a lease starts serves it, and firebase-tools still READS it. A
384
+ web-framework target (`"source": "."`, a Next.js app) makes 15.33 refuse to
385
+ start ANY emulator without the webframeworks experiment — Tapuat's run
386
+ 20261021-130009-exploration-everyday-user died there on simon-linux-1 while
387
+ 15.28 on the studio never checked."""
367
388
  cfg = json.loads((Path(cwd) / "firebase.json").read_text())
368
389
  emu = cfg.setdefault("emulators", {})
390
+ if only and "hosting" not in only:
391
+ cfg.pop("hosting", None)
392
+ emu.pop("hosting", None)
369
393
  for svc in [k for k in emu if k != "singleProjectMode"]:
370
394
  if svc in ports and isinstance(emu[svc], dict):
371
395
  emu[svc]["port"] = ports[svc]
@@ -775,7 +799,7 @@ class Broker:
775
799
 
776
800
  def _boot(self, cwd, block, only, project_id, label, on_spawn=None, extra_env=None):
777
801
  all_ports = _block_ports(block)
778
- tmp = _write_injected_config(cwd, all_ports)
802
+ tmp = _write_injected_config(cwd, all_ports, only)
779
803
  # Readiness/liveness track ONLY the data services actually started (respect
780
804
  # --only, else the project's declared emulators; skip ui/hub/logging) — else
781
805
  # we'd wait forever for ports (functions, pubsub, …) that never open.
@@ -804,9 +828,9 @@ class Broker:
804
828
  # daemon's env — the functions emulator runtime inherits it, which is
805
829
  # the only way a CALLER can set per-suite env (e.g. LLM_CACHE_DIR)
806
830
  # when the daemon, not the caller, spawns the emulator.
807
- spawn_env = {**os.environ, **project_env(cwd), "TMPDIR": str(suite_tmp)}
831
+ spawn_env = {**os.environ, **_with_ports(project_env(cwd), all_ports), "TMPDIR": str(suite_tmp)}
808
832
  if extra_env:
809
- spawn_env.update({str(k): str(v) for k, v in dict(extra_env).items()})
833
+ spawn_env.update(_with_ports({str(k): str(v) for k, v in dict(extra_env).items()}, all_ports))
810
834
  with open(log, "w") as lf:
811
835
  proc = subprocess.Popen(cmd, cwd=cwd, stdout=lf, stderr=subprocess.STDOUT, start_new_session=True, env=spawn_env)
812
836
  if on_spawn:
@@ -1841,7 +1865,7 @@ def main():
1841
1865
  me = os.getpid() # this process owns the reservation for the run
1842
1866
  res = ok(_call("POST", "/allocate", {"owner_pid": me, "owner_start": _proc_start(me), "label": "exec"}), "exec")
1843
1867
  base, lid = res["base"], res["leaseId"]
1844
- tmp = _write_injected_config(cwd, _block_ports(base))
1868
+ tmp = _write_injected_config(cwd, _block_ports(base), only)
1845
1869
  try:
1846
1870
  run = ["firebase", "emulators:exec", "--config", tmp]
1847
1871
  if only:
@@ -1850,7 +1874,7 @@ def main():
1850
1874
  # The project's env beats the caller's AMBIENT env here: a key
1851
1875
  # exported in some shell for a one-off script is exactly what
1852
1876
  # harbormaster.env exists to keep out of the suite (project_env).
1853
- rc = subprocess.call(run, cwd=cwd, env={**os.environ, **project_env(cwd)})
1877
+ rc = subprocess.call(run, cwd=cwd, env={**os.environ, **_with_ports(project_env(cwd), _block_ports(base))})
1854
1878
  finally:
1855
1879
  try:
1856
1880
  _call("POST", "/release", {"leaseId": lid})
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@simon-says-build/harbormaster",
3
- "version": "0.14.0",
3
+ "version": "0.14.2",
4
4
  "description": "A localhost daemon that leases each caller its own port block — isolated Firebase emulator sandboxes, Metro/Expo ports, private iOS simulators — reaped when the caller exits. No port collisions, no shared instances, no pkill.",
5
5
  "bin": {
6
6
  "harbormaster": "harbormaster",