From 815ae1725f1443938bbfce52611b2030d8eca965 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andre=CC=81=20Lange?= Date: Sun, 15 Mar 2026 03:15:38 +0100 Subject: [PATCH 1/2] feat(onboarding): add markdown report export --- CHANGELOG.md | 1 + README.md | 6 ++ docs/ONBOARDING.md | 1 + foundrygate/onboarding.py | 115 ++++++++++++++++++++++++++ scripts/foundrygate-onboarding-report | 8 +- tests/test_onboarding.py | 6 ++ 6 files changed, 136 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e72593d..4b34318 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,6 +17,7 @@ The format is intentionally lightweight and human-readable. Group entries by rel - Added starter provider snippets for cloud, local-worker, and image-provider setups under `docs/examples/` - Added matching provider `.env` starter files for cloud, local-worker, and image-provider onboarding flows - Added provider env placeholder checks to `foundrygate-doctor` so missing `.env` values are surfaced before rollout +- Added `--markdown` output to `foundrygate-onboarding-report` so onboarding state can be pasted into issues, PRs, or hand-off notes ## v0.7.0 - 2026-03-12 diff --git a/README.md b/README.md index 45a5dfe..b42e0df 100644 --- a/README.md +++ b/README.md @@ -852,6 +852,12 @@ Provider starter snippets for the first rollout path live under [docs/examples]( - [provider-local-worker.env.example](./docs/examples/provider-local-worker.env.example) - [provider-image-provider.yaml](./docs/examples/provider-image-provider.yaml) - [provider-image-provider.env.example](./docs/examples/provider-image-provider.env.example) + +If you want to paste the current onboarding state into a ticket or PR, run: + +```bash +./scripts/foundrygate-onboarding-report --markdown +``` | `foundrygate-install` | Installs the unit file, creates `/var/lib/foundrygate`, creates helper symlinks, reloads `systemd`, and starts the service | | `foundrygate-start` | Runs `systemctl start foundrygate.service` | | `foundrygate-stop` | Runs `systemctl stop foundrygate.service` | diff --git a/docs/ONBOARDING.md b/docs/ONBOARDING.md index 7d46a5e..632cf26 100644 --- a/docs/ONBOARDING.md +++ b/docs/ONBOARDING.md @@ -72,6 +72,7 @@ For many-provider rollouts, run the onboarding report after every provider chang ```bash ./scripts/foundrygate-onboarding-report +./scripts/foundrygate-onboarding-report --markdown ./scripts/foundrygate-onboarding-report --json ./scripts/foundrygate-onboarding-validate ``` diff --git a/foundrygate/onboarding.py b/foundrygate/onboarding.py index 0d37efa..003d5b0 100644 --- a/foundrygate/onboarding.py +++ b/foundrygate/onboarding.py @@ -534,6 +534,121 @@ def render_onboarding_report(report: dict[str, Any]) -> str: return "\n".join(lines) + "\n" +def render_onboarding_report_markdown(report: dict[str, Any]) -> str: + """Render the onboarding report as Markdown.""" + provider_block = report["providers"] + client_block = report["clients"] + routing_block = report["routing"] + rollout_block = report["provider_rollout"] + ops_block = report["operations"] + integration_block = report["integrations"] + env_block = report["env"] + + lines = [ + "# FoundryGate Onboarding Report", + "", + f"- Config: `{report['config_path']}`", + f"- Env: `{report['env_file']}`", + "", + "## Providers", + f"- Total: {provider_block['total']}", + f"- Ready: {provider_block['ready']}", + f"- Not ready: {provider_block['not_ready']}", + f"- Local workers: {provider_block['local_workers']}", + f"- Image-capable: {provider_block['image_capable']}", + ] + + env_requirements = env_block.get("provider_requirements", {}) + if env_requirements.get("missing"): + lines.append( + "- Missing provider env: " + + ", ".join(f"`{item}`" for item in env_requirements["missing"]) + ) + + if provider_block["items"]: + lines.extend(["", "### Provider Inventory"]) + for item in provider_block["items"]: + readiness = "ready" if item["ready"] else f"not ready ({item['readiness_reason']})" + lines.append( + f"- `{item['name']}`: {item['contract']} / {item['backend']} / " + f"{item['tier'] or 'default'} / {readiness}" + ) + + lines.extend( + [ + "", + "## Clients", + f"- Profiles enabled: {client_block['profiles_enabled']}", + f"- Default profile: `{client_block['default_profile']}`", + "- Presets: " + + (", ".join(f"`{item}`" for item in client_block["presets"]) or "none"), + f"- Profiles: {client_block['profile_count']}", + f"- Rules: {client_block['rule_count']}", + ] + ) + + if client_block["matrix"]: + lines.extend(["", "### Client Matrix"]) + for row in client_block["matrix"]: + default_text = " (default)" if row["default"] else "" + lines.append(f"- `{row['name']}`{default_text}: {row['source']}") + lines.append(f" - Match: {row['matched_by']}") + lines.append(f" - Intent: {'; '.join(row['routing_intent'])}") + + lines.extend( + [ + "", + "## Routing", + "- Fallback chain: " + + (", ".join(f"`{item}`" for item in routing_block["fallback_chain"]) or "none"), + f"- Policy layer: {routing_block['policy_layer_enabled']} " + f"({routing_block['policy_rule_count']} rules)", + f"- Request hooks: {routing_block['request_hooks_enabled']} " + f"({routing_block['request_hook_count']} hooks)", + "", + "## Provider Rollout", + "- Stage 1 primary: " + + (", ".join(f"`{item}`" for item in rollout_block["stage_1_primary"]) or "none"), + "- Stage 2 secondary: " + + (", ".join(f"`{item}`" for item in rollout_block["stage_2_secondary"]) or "none"), + "- Stage 3 modality: " + + (", ".join(f"`{item}`" for item in rollout_block["stage_3_modality"]) or "none"), + ] + ) + + if rollout_block["fallback_targets"]: + lines.append("- Fallback targets:") + for item in rollout_block["fallback_targets"]: + readiness = "ready" if item["ready"] else "not ready" + lines.append(f" - `{item['name']}`: {readiness}") + + lines.extend( + [ + "", + "## Operations", + f"- Update checks: {ops_block['update_checks_enabled']}", + f"- Auto update: {ops_block['auto_update_enabled']}", + f"- Rollout ring: `{ops_block['rollout_ring']}`", + "", + "## Integration Quickstarts", + ] + ) + + for client_name, data in integration_block.items(): + readiness = "ready" if data["recommended"] else "needs preset or custom profile" + lines.append(f"- `{client_name}`: {readiness}") + lines.append(f" - Header: `{data['header']}`") + lines.append(f" - Profile: `{data['profile']}`") + for snippet_line in data["snippet"]: + lines.append(f" - Example: `{snippet_line}`") + + if report["suggestions"]: + lines.extend(["", "## Suggestions"]) + lines.extend(f"- {item}" for item in report["suggestions"]) + + return "\n".join(lines) + "\n" + + def render_onboarding_validation(validation: dict[str, Any]) -> str: """Render onboarding validation results as plain text.""" lines = [ diff --git a/scripts/foundrygate-onboarding-report b/scripts/foundrygate-onboarding-report index d6bd9aa..623b447 100644 --- a/scripts/foundrygate-onboarding-report +++ b/scripts/foundrygate-onboarding-report @@ -14,7 +14,11 @@ python3 - <<'PY' import json import os -from foundrygate.onboarding import build_onboarding_report, render_onboarding_report +from foundrygate.onboarding import ( + build_onboarding_report, + render_onboarding_report, + render_onboarding_report_markdown, +) report = build_onboarding_report( config_path=os.environ["FOUNDRYGATE_ONBOARDING_CONFIG"], @@ -23,6 +27,8 @@ report = build_onboarding_report( if os.environ["FOUNDRYGATE_ONBOARDING_MODE"] == "--json": print(json.dumps(report, indent=2, sort_keys=True)) +elif os.environ["FOUNDRYGATE_ONBOARDING_MODE"] == "--markdown": + print(render_onboarding_report_markdown(report), end="") else: print(render_onboarding_report(report), end="") PY diff --git a/tests/test_onboarding.py b/tests/test_onboarding.py index 05d6086..3c8cfd1 100644 --- a/tests/test_onboarding.py +++ b/tests/test_onboarding.py @@ -7,6 +7,7 @@ build_onboarding_validation, collect_provider_env_requirements, render_onboarding_report, + render_onboarding_report_markdown, render_onboarding_validation, ) @@ -114,6 +115,11 @@ def test_onboarding_report_marks_local_worker_ready(tmp_path: Path): assert "Integration quickstarts" in text assert "header: X-FoundryGate-Client: codex" in text + markdown = render_onboarding_report_markdown(report) + assert "# FoundryGate Onboarding Report" in markdown + assert "## Provider Rollout" in markdown + assert "`local-worker`" in markdown + def test_onboarding_validation_blocks_missing_env_and_unready_providers( tmp_path: Path, monkeypatch From fcac024502d80e0676140c8fffee68ff67ded484 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andre=CC=81=20Lange?= Date: Sun, 15 Mar 2026 03:22:01 +0100 Subject: [PATCH 2/2] style(onboarding): apply ruff formatting --- foundrygate/onboarding.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/foundrygate/onboarding.py b/foundrygate/onboarding.py index 003d5b0..161370f 100644 --- a/foundrygate/onboarding.py +++ b/foundrygate/onboarding.py @@ -580,8 +580,7 @@ def render_onboarding_report_markdown(report: dict[str, Any]) -> str: "## Clients", f"- Profiles enabled: {client_block['profiles_enabled']}", f"- Default profile: `{client_block['default_profile']}`", - "- Presets: " - + (", ".join(f"`{item}`" for item in client_block["presets"]) or "none"), + "- Presets: " + (", ".join(f"`{item}`" for item in client_block["presets"]) or "none"), f"- Profiles: {client_block['profile_count']}", f"- Rules: {client_block['rule_count']}", ]