diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..7e9e847 --- /dev/null +++ b/.github/workflows/ci.yml @@ -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 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..1d2841c --- /dev/null +++ b/.gitignore @@ -0,0 +1,11 @@ +__pycache__/ +.pytest_cache/ +.venv/ +.env/ +*.pyc +*.pyo +*.pyd +*.egg-info/ +.dist-info/ +dist/ +build/ diff --git a/README.md b/README.md index 2c9b4a6..298cb52 100644 --- a/README.md +++ b/README.md @@ -1 +1,107 @@ -# platform-bootstrap-api \ No newline at end of file +# 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 diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..bb540ba --- /dev/null +++ b/pyproject.toml @@ -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"] diff --git a/src/platform_bootstrap_api/__init__.py b/src/platform_bootstrap_api/__init__.py new file mode 100644 index 0000000..1be532e --- /dev/null +++ b/src/platform_bootstrap_api/__init__.py @@ -0,0 +1 @@ +"""Platform Bootstrap API package.""" diff --git a/src/platform_bootstrap_api/main.py b/src/platform_bootstrap_api/main.py new file mode 100644 index 0000000..4b9cc72 --- /dev/null +++ b/src/platform_bootstrap_api/main.py @@ -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) diff --git a/src/platform_bootstrap_api/schemas.py b/src/platform_bootstrap_api/schemas.py new file mode 100644 index 0000000..4537c38 --- /dev/null +++ b/src/platform_bootstrap_api/schemas.py @@ -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] diff --git a/tests/test_health.py b/tests/test_health.py new file mode 100644 index 0000000..81bf75d --- /dev/null +++ b/tests/test_health.py @@ -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