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
31 changes: 31 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
name: ci

on:
push:
branches: ["main"]
pull_request:

jobs:
test:
runs-on: ubuntu-latest
strategy:
matrix:
python-version: ["3.10", "3.11", "3.12"]

steps:
- name: Checkout
uses: actions/checkout@v4

- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: ${{ matrix.python-version }}

- name: Upgrade pip
run: python -m pip install -U pip

- name: Install dependencies
run: python -m pip install -e .[dev]

- name: Run tests
run: pytest -q
11 changes: 11 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
__pycache__/
.pytest_cache/
.venv/
.env/
*.pyc
*.pyo
*.pyd
*.egg-info/
.dist-info/
dist/
build/
108 changes: 107 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1 +1,107 @@
# platform-bootstrap-api
# platform-bootstrap-api

A privacy-safe FastAPI service that plans repository bootstrap actions.

This project models a common platform engineering pattern:
exposing baseline repository standards (ownership, CI, and branch protection)
through an API instead of relying on manual setup or tribal knowledge.

Default behavior is **dry-run**, ensuring no external writes occur unless explicitly implemented.

---

## Why this exists

Platform teams often standardize:

- Repository hygiene
- CODEOWNERS enforcement
- CI defaults
- Security documentation
- Branch protection policies

Rather than configuring each repository manually, this service demonstrates
how those guardrails can be encoded into an internal platform API.

The focus is repeatability, safety, and clarity.

---

## Features

- Health check endpoint (`/healthz`)
- Bootstrap planning endpoint (`/bootstrap`)
- Dry-run by default for privacy and safety
- Clear separation between API layer and bootstrap logic

---

## Local development

Install dependencies:

```bash
python -m pip install -e .[dev]
```

Run the API:

```bash
uvicorn platform_bootstrap_api.main:app --reload --port 8000
```

---

## API usage

Health check:

```bash
curl http://localhost:8000/healthz
```

Bootstrap (dry-run default):

```bash
curl -X POST http://localhost:8000/bootstrap \
-H "Content-Type: application/json" \
-d '{"owner":"acme","repo":"demo-repo"}'
```

Example response:

```json
{
"ok": true,
"dry_run": true,
"actions": [
"ensure repo exists: acme/demo-repo",
"upsert CODEOWNERS -> @my-org/platform",
"upsert SECURITY.md",
"upsert CONTRIBUTING.md",
"upsert .github/workflows/ci.yml",
"set branch protection on main"
]
}
```

---

## Architecture

```
Client → FastAPI → Bootstrap Planner → (future) GitHub API client
```

- FastAPI handles request validation
- Bootstrap planner defines platform policy
- GitHub client abstraction enables future real execution

---

## Roadmap

- Integrate with `platform-bootstrap` package for real execution
- Add optional authentication header
- Support policy packs and organization-wide defaults
- Containerized deployment example
31 changes: 31 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
[project]
name = "platform-bootstrap-api"
version = "0.1.0"
description = "Privacy-safe FastAPI service for platform bootstrap planning (dry-run)."
readme = "README.md"
requires-python = ">=3.10"
dependencies = [
"fastapi",
"uvicorn",
"pydantic",
]

[project.optional-dependencies]
dev = [
"pytest",
"httpx",
]

[build-system]
requires = ["setuptools>=61.0"]
build-backend = "setuptools.build_meta"

[tool.setuptools]
package-dir = {"" = "src"}

[tool.setuptools.packages.find]
where = ["src"]

[tool.pytest.ini_options]
addopts = "-q"
testpaths = ["tests"]
1 change: 1 addition & 0 deletions src/platform_bootstrap_api/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
"""Platform Bootstrap API package."""
25 changes: 25 additions & 0 deletions src/platform_bootstrap_api/main.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
from __future__ import annotations

from fastapi import FastAPI

from platform_bootstrap_api.schemas import BootstrapRequest, BootstrapResponse

app = FastAPI(title="platform-bootstrap-api")


@app.get("/healthz")
def healthz() -> dict:
return {"ok": True}


@app.post("/bootstrap", response_model=BootstrapResponse)
def bootstrap(request: BootstrapRequest) -> BootstrapResponse:
actions = [
f"ensure repo exists: {request.owner}/{request.repo}",
f"upsert CODEOWNERS -> {request.codeowners}",
"upsert SECURITY.md",
"upsert CONTRIBUTING.md",
"upsert .github/workflows/ci.yml",
f"set branch protection on {request.default_branch}",
]
return BootstrapResponse(ok=True, dry_run=request.dry_run, actions=actions)
22 changes: 22 additions & 0 deletions src/platform_bootstrap_api/schemas.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
from __future__ import annotations

from typing import List

from pydantic import BaseModel, Field


class BootstrapRequest(BaseModel):
owner: str = Field(..., description="Repository owner or organization.")
repo: str = Field(..., description="Repository name.")
default_branch: str = Field("main", description="Default branch name.")
codeowners: str = Field(
"@my-org/platform",
description="CODEOWNERS entry for the repository.",
)
dry_run: bool = Field(True, description="If true, no external writes occur.")


class BootstrapResponse(BaseModel):
ok: bool
dry_run: bool
actions: List[str]
22 changes: 22 additions & 0 deletions tests/test_health.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
from fastapi.testclient import TestClient

from platform_bootstrap_api.main import app


client = TestClient(app)


def test_healthz_ok() -> None:
response = client.get("/healthz")
assert response.status_code == 200
assert response.json() == {"ok": True}


def test_bootstrap_ok() -> None:
payload = {"owner": "octo-org", "repo": "demo-repo"}
response = client.post("/bootstrap", json=payload)
assert response.status_code == 200
body = response.json()
assert body["ok"] is True
assert isinstance(body["actions"], list)
assert body["dry_run"] is True