Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
6 changes: 6 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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` |
Expand Down
1 change: 1 addition & 0 deletions docs/ONBOARDING.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
```
Expand Down
114 changes: 114 additions & 0 deletions foundrygate/onboarding.py
Original file line number Diff line number Diff line change
Expand Up @@ -534,6 +534,120 @@ 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 = [
Expand Down
8 changes: 7 additions & 1 deletion scripts/foundrygate-onboarding-report
Original file line number Diff line number Diff line change
Expand Up @@ -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"],
Expand All @@ -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
6 changes: 6 additions & 0 deletions tests/test_onboarding.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
build_onboarding_validation,
collect_provider_env_requirements,
render_onboarding_report,
render_onboarding_report_markdown,
render_onboarding_validation,
)

Expand Down Expand Up @@ -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
Expand Down
Loading