@simon-says-build/harbormaster 0.11.1 → 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.
Files changed (3) hide show
  1. package/README.md +17 -0
  2. package/harbormaster +46 -3
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -137,3 +137,20 @@ stale marker means the service is gone, and clients spawn as before. Without
137
137
  this, the broker belonged to whichever client happened to start it: on
138
138
  simon-linux-1 that was a CI job, whose orphan cleanup killed the broker and
139
139
  every emulator it ran for other jobs mid-suite. `python3 test/managed.py`.
140
+
141
+ ## A project's own suite env (0.12.0)
142
+
143
+ `--fn-env` is per caller, so a setting every suite of a project needs had to
144
+ be repeated by every caller (session tests, e2e, agent runs), and the one that
145
+ forgot ran against production. A project now names it once, in
146
+ `harbormaster.env` next to its `firebase.json`:
147
+
148
+ # every emulator suite of this project runs under the TEST service account
149
+ GOOGLE_APPLICATION_CREDENTIALS=./impulse-test-bq.json
150
+
151
+ `KEY=VALUE` lines, `#` comments. A value starting with `./` or `../` is made
152
+ absolute against the project, since the emulator runs functions (and
153
+ extensions) in other directories. `acquire`: on top of the daemon's scrubbed
154
+ env, under the caller's explicit `--fn-env`. `exec`: on top of the caller's
155
+ ambient env, so a key exported in some shell can't leak into the suite.
156
+ `python3 test/project_env.py`.
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.11.1"
87
+ VERSION = "0.12.0"
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
@@ -699,7 +699,7 @@ class Broker:
699
699
  # daemon's env — the functions emulator runtime inherits it, which is
700
700
  # the only way a CALLER can set per-suite env (e.g. LLM_CACHE_DIR)
701
701
  # when the daemon, not the caller, spawns the emulator.
702
- spawn_env = {**os.environ, "TMPDIR": str(suite_tmp)}
702
+ spawn_env = {**os.environ, **project_env(cwd), "TMPDIR": str(suite_tmp)}
703
703
  if extra_env:
704
704
  spawn_env.update({str(k): str(v) for k, v in dict(extra_env).items()})
705
705
  with open(log, "w") as lf:
@@ -1333,6 +1333,46 @@ def _daemon_version():
1333
1333
  return None
1334
1334
 
1335
1335
 
1336
+ # A PROJECT'S OWN SUITE ENV: <cwd>/harbormaster.env.
1337
+ #
1338
+ # --fn-env is per CALLER, so a setting every suite of a project needs (the
1339
+ # credentials its emulator must run under, say) had to be repeated by every
1340
+ # caller — session tests, e2e, agent runs — and the one that forgot ran
1341
+ # against production. impulse-functions (2026-10-16): its BigQuery export
1342
+ # extensions run under the Extensions emulator and stream to whatever project
1343
+ # the ambient credentials can reach; only a test service account keeps that
1344
+ # test-only. So the project names it once, in the repo:
1345
+ #
1346
+ # # harbormaster.env, next to firebase.json
1347
+ # GOOGLE_APPLICATION_CREDENTIALS=./impulse-test-bq.json
1348
+ #
1349
+ # KEY=VALUE lines; blank lines and `#` comments skipped; a value starting
1350
+ # with ./ or ../ is a path relative to the project, made absolute (the
1351
+ # emulator's functions run in other directories). acquire: on top of the
1352
+ # daemon's (scrubbed) env, UNDER the caller's explicit --fn-env. exec: on top
1353
+ # of the caller's ambient env, which is not a choice. Pure; missing file = {}.
1354
+ PROJECT_ENV_FILE = "harbormaster.env"
1355
+
1356
+
1357
+ def project_env(cwd):
1358
+ f = Path(cwd) / PROJECT_ENV_FILE
1359
+ if not f.is_file():
1360
+ return {}
1361
+ out = {}
1362
+ for raw in f.read_text().splitlines():
1363
+ line = raw.strip()
1364
+ if not line or line.startswith("#") or "=" not in line:
1365
+ continue
1366
+ k, v = line.split("=", 1)
1367
+ k, v = k.strip(), v.strip()
1368
+ if len(v) >= 2 and v[0] == v[-1] and v[0] in "'\"":
1369
+ v = v[1:-1]
1370
+ if v.startswith("./") or v.startswith("../"):
1371
+ v = str((Path(cwd) / v).resolve())
1372
+ out[k] = v
1373
+ return out
1374
+
1375
+
1336
1376
  def _ver_tuple(v):
1337
1377
  try:
1338
1378
  return tuple(int(x) for x in str(v).split("."))
@@ -1626,7 +1666,10 @@ def main():
1626
1666
  if only:
1627
1667
  run += ["--only", ",".join(only)]
1628
1668
  run.append(script)
1629
- rc = subprocess.call(run, cwd=cwd)
1669
+ # The project's env beats the caller's AMBIENT env here: a key
1670
+ # exported in some shell for a one-off script is exactly what
1671
+ # harbormaster.env exists to keep out of the suite (project_env).
1672
+ rc = subprocess.call(run, cwd=cwd, env={**os.environ, **project_env(cwd)})
1630
1673
  finally:
1631
1674
  try:
1632
1675
  _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.11.1",
3
+ "version": "0.12.0",
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",