diff --git a/workspace/preflight.py b/workspace/preflight.py index d6123f25..e1929f3e 100644 --- a/workspace/preflight.py +++ b/workspace/preflight.py @@ -180,16 +180,38 @@ def run_preflight(config: WorkspaceConfig, config_path: str) -> PreflightReport: required_env = list(entry.get("required_env") or []) break + # Smoke mode skips the auth-env block: the boot smoke (CI publish-image, + # issue #2275) exercises executor.execute() against stub deps, never + # hits the real provider, and CI cannot enumerate every adapter's auth + # env without forming a maintenance treadmill. Hermes 2026-05-03 outage: + # template smoke crashed for two cycles because molecule-ci injected + # CLAUDE_CODE_OAUTH_TOKEN/ANTHROPIC_API_KEY/etc. but not HERMES_API_KEY. + # Bypass here means new templates can ship without the workflow + # learning their env names. + smoke_mode = os.environ.get("MOLECULE_SMOKE_MODE", "").strip().lower() in ( + "1", "true", "yes", "on", + ) for env_var in required_env: - if not os.environ.get(env_var): - report.failures.append( + if os.environ.get(env_var): + continue + if smoke_mode: + report.warnings.append( PreflightIssue( - severity="fail", + severity="warn", title="Required env", - detail=f"Missing required environment variable: {env_var}", - fix=f"Set {env_var} via the secrets API (global or workspace-level).", + detail=f"Missing {env_var} (skipped — MOLECULE_SMOKE_MODE)", + fix="", ) ) + continue + report.failures.append( + PreflightIssue( + severity="fail", + title="Required env", + detail=f"Missing required environment variable: {env_var}", + fix=f"Set {env_var} via the secrets API (global or workspace-level).", + ) + ) # Backward compat: if legacy auth_token_file is set, warn but don't block # if the token is available via required_env or auth_token_env. diff --git a/workspace/tests/test_preflight.py b/workspace/tests/test_preflight.py index febf536a..063dcb8f 100644 --- a/workspace/tests/test_preflight.py +++ b/workspace/tests/test_preflight.py @@ -286,6 +286,55 @@ def test_required_env_empty_list_passes(tmp_path): assert report.ok is True +def test_required_env_skipped_in_smoke_mode(tmp_path, monkeypatch): + """MOLECULE_SMOKE_MODE=1 demotes Required-env failures to warnings. + + Boot smoke (issue #2275) exercises executor.execute() against stub + deps and never hits the real provider, so missing auth env is not + a real blocker. Without this bypass, every adapter that introduces + a new auth env var (HERMES_API_KEY, OPENROUTER_API_KEY, etc.) + would silently break the publish-image gate until molecule-ci's + fake-env list catches up — the 2026-05-03 hermes outage. The + warning still surfaces in the report so unset env doesn't go + completely silent. + """ + monkeypatch.delenv("HERMES_API_KEY", raising=False) + monkeypatch.setenv("MOLECULE_SMOKE_MODE", "1") + + config = make_config( + runtime_config=RuntimeConfig(required_env=["HERMES_API_KEY"]), + ) + + report = run_preflight(config, str(tmp_path)) + + assert report.ok is True + assert any( + issue.title == "Required env" and "HERMES_API_KEY" in issue.detail + for issue in report.warnings + ), "smoke-mode bypass should still warn so unset env stays visible" + assert not any( + issue.title == "Required env" for issue in report.failures + ) + + +def test_required_env_smoke_mode_off_still_fails(tmp_path, monkeypatch): + """Sanity: smoke bypass is OFF when MOLECULE_SMOKE_MODE is unset.""" + monkeypatch.delenv("HERMES_API_KEY", raising=False) + monkeypatch.delenv("MOLECULE_SMOKE_MODE", raising=False) + + config = make_config( + runtime_config=RuntimeConfig(required_env=["HERMES_API_KEY"]), + ) + + report = run_preflight(config, str(tmp_path)) + + assert report.ok is False + assert any( + issue.title == "Required env" and "HERMES_API_KEY" in issue.detail + for issue in report.failures + ) + + # ---------- Per-model required_env (models[] override) ----------