From 6a815a501ebf1007b864722508eaa2c2105e9fed Mon Sep 17 00:00:00 2001 From: Gaurav Joshi Date: Thu, 12 Feb 2026 11:01:23 -0700 Subject: [PATCH 1/2] Initial project scaffold for platform-bootstrap Add initial package scaffold and tooling for a GitHub repository bootstrapper: project metadata (pyproject.toml), README, MIT LICENSE, .gitignore, CI workflow, and tests. Implement core package layout under src/platform_bootstrap with a CLI, bootstrap orchestration (dry-run safe), and a GitHubClient abstraction (stubs for upsert_file, branch protection, and repo creation). Includes a simple smoke test exercising dry-run behavior. TODO: implement real GitHub contents and branch-protection API calls and repository creation for non-dry-run mode. --- .github/workflows/ci.yml | 22 ++++ .gitignore | 17 ++++ LICENSE | 21 ++++ README.md | 112 +++++++++++++++++++- pyproject.toml | 25 +++++ src/platform_bootstrap/__init__.py | 4 + src/platform_bootstrap/bootstrap.py | 129 ++++++++++++++++++++++++ src/platform_bootstrap/cli.py | 47 +++++++++ src/platform_bootstrap/github_client.py | 74 ++++++++++++++ tests/test_smoke.py | 11 ++ 10 files changed, 461 insertions(+), 1 deletion(-) create mode 100644 .github/workflows/ci.yml create mode 100644 .gitignore create mode 100644 LICENSE create mode 100644 pyproject.toml create mode 100644 src/platform_bootstrap/__init__.py create mode 100644 src/platform_bootstrap/bootstrap.py create mode 100644 src/platform_bootstrap/cli.py create mode 100644 src/platform_bootstrap/github_client.py create mode 100644 tests/test_smoke.py diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..9fb3e21 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,22 @@ +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: + - uses: actions/checkout@v4 + - uses: actions/setup-python@v5 + with: + python-version: ${{ matrix.python-version }} + - name: Install + run: python -m pip install -e . + - name: Test + run: pytest diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..136bd44 --- /dev/null +++ b/.gitignore @@ -0,0 +1,17 @@ +__pycache__/ +*.py[cod] +*.egg-info/ +.eggs/ +.dist/ +build/ +dist/ +.venv/ +venv/ +.env/ +.pytest_cache/ +.coverage +.coverage.* +htmlcov/ +.mypy_cache/ +.ruff_cache/ +.DS_Store diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..14fac91 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2026 + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md index 73c3ede..c268b38 100644 --- a/README.md +++ b/README.md @@ -1 +1,111 @@ -# platform-bootstrap \ No newline at end of file +# platform-bootstrap + +A privacy-safe, generic bootstrapper for GitHub repositories. + +This project demonstrates a common platform engineering pattern: +automating baseline standards (ownership, CI, contribution guidance, +and branch protection) so teams can move quickly without sacrificing +guardrails. + +It is intentionally designed to run safely in dry-run mode and does not +perform destructive writes unless explicitly configured. + +--- + +## Why this exists + +Platform teams frequently standardize: + +- Repository hygiene +- CODEOWNERS enforcement +- CI defaults +- Security documentation +- Branch protection rules + +Rather than relying on manual setup or tribal knowledge, this tool encodes +those practices into repeatable automation. + +The goal is consistency, not rigidity. + +--- + +## Features + +- Repository bootstrap CLI +- Dry-run mode for safe previews +- GitHub API client abstraction +- Baseline file scaffolding: + - `CODEOWNERS` + - `SECURITY.md` + - `CONTRIBUTING.md` + - CI workflow template +- Branch protection configuration (stubbed for v1) + +--- + +## Install + +```bash +python -m pip install -e . +``` + +--- + +## Usage + +```bash +platform-bootstrap bootstrap \ + --owner my-org \ + --repo my-repo \ + --default-branch main \ + --codeowners "@my-org/platform" \ + --dry-run +``` + +--- + +## Behavior + +The bootstrapper will: + +- Ensure the repository exists +- Upsert baseline files +- Add a minimal CI workflow +- Configure branch protection + +In dry-run mode, it prints intended actions without network writes. + +Non-dry-run mode retains GitHub API method signatures and provides +clear TODO markers for full implementation. + +--- + +## Architecture + +``` +CLI → Bootstrap Service → GitHubClient → GitHub API +``` + +- CLI handles argument parsing +- Bootstrap service defines platform policy +- GitHubClient abstracts API interaction +- GitHub API executes repository configuration + +--- + +## Design Notes + +- Uses a `src/` layout for clean packaging. +- Separates CLI, bootstrap logic, and GitHub API client. +- Designed to evolve into a reusable internal SDK or service. +- Emphasizes safety-first automation via dry-run support. + +--- + +## Roadmap + +- Full GitHub file upsert implementation +- Org-level policy packs +- Template repository support +- Optional FastAPI wrapper for service deployment +- Policy-as-code integration patterns diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..dfbea78 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,25 @@ +[build-system] +requires = ["setuptools>=68", "wheel"] +build-backend = "setuptools.build_meta" + +[project] +name = "platform-bootstrap" +version = "0.1.0" +description = "A privacy-safe bootstrapper for GitHub repositories." +readme = "README.md" +requires-python = ">=3.10" +license = {text = "MIT"} +authors = [ + {name = "Platform Engineering", email = "platform@example.com"} +] +dependencies = [ + "requests>=2.31" +] + +[project.scripts] +platform-bootstrap = "platform_bootstrap.cli:main" + +[tool.pytest.ini_options] +minversion = "7.0" +addopts = "-q" +testpaths = ["tests"] diff --git a/src/platform_bootstrap/__init__.py b/src/platform_bootstrap/__init__.py new file mode 100644 index 0000000..7f93114 --- /dev/null +++ b/src/platform_bootstrap/__init__.py @@ -0,0 +1,4 @@ +"""Platform bootstrap package.""" + +__all__ = ["__version__"] +__version__ = "0.1.0" diff --git a/src/platform_bootstrap/bootstrap.py b/src/platform_bootstrap/bootstrap.py new file mode 100644 index 0000000..a489081 --- /dev/null +++ b/src/platform_bootstrap/bootstrap.py @@ -0,0 +1,129 @@ +from __future__ import annotations + +from dataclasses import dataclass + +from platform_bootstrap.github_client import GitHubClient + + +@dataclass +class BootstrapOptions: + owner: str + repo: str + default_branch: str + codeowners: str + dry_run: bool = False + + +def _codeowners_content(codeowners: str) -> str: + return f"* {codeowners}\n" + + +def _security_md() -> str: + return ( + "# Security Policy\n\n" + "If you discover a security issue, please report it privately.\n" + "Do not open public issues with sensitive details.\n" + ) + + +def _contributing_md() -> str: + return ( + "# Contributing\n\n" + "Thanks for your interest in improving this project.\n" + "Please open an issue to discuss major changes before submitting a PR.\n" + ) + + +def _ci_workflow() -> str: + return ( + "name: CI\n\n" + "on:\n" + " push:\n" + " branches: [\"main\"]\n" + " pull_request:\n\n" + "jobs:\n" + " test:\n" + " runs-on: ubuntu-latest\n" + " strategy:\n" + " matrix:\n" + " python-version: [\"3.10\", \"3.11\", \"3.12\"]\n" + " steps:\n" + " - uses: actions/checkout@v4\n" + " - uses: actions/setup-python@v5\n" + " with:\n" + " python-version: ${{ matrix.python-version }}\n" + " - name: Install\n" + " run: python -m pip install -e .\n" + " - name: Test\n" + " run: pytest\n" + ) + + +def bootstrap_repo( + *, + owner: str, + repo: str, + default_branch: str, + codeowners: str, + dry_run: bool = False, +) -> None: + options = BootstrapOptions( + owner=owner, + repo=repo, + default_branch=default_branch, + codeowners=codeowners, + dry_run=dry_run, + ) + + client = GitHubClient.from_env(dry_run=options.dry_run) + + if options.dry_run: + print("[dry-run] starting bootstrap") + + exists = client.ensure_repo(options.owner, options.repo) + if not exists and not options.dry_run: + raise NotImplementedError( + "TODO: Implement repository creation for non-existent repos." + ) + + client.upsert_file( + owner=options.owner, + repo=options.repo, + path="CODEOWNERS", + content=_codeowners_content(options.codeowners), + message="chore: add CODEOWNERS", + branch=options.default_branch, + ) + client.upsert_file( + owner=options.owner, + repo=options.repo, + path="SECURITY.md", + content=_security_md(), + message="chore: add SECURITY policy", + branch=options.default_branch, + ) + client.upsert_file( + owner=options.owner, + repo=options.repo, + path="CONTRIBUTING.md", + content=_contributing_md(), + message="chore: add CONTRIBUTING guide", + branch=options.default_branch, + ) + client.upsert_file( + owner=options.owner, + repo=options.repo, + path=".github/workflows/ci.yml", + content=_ci_workflow(), + message="chore: add CI workflow", + branch=options.default_branch, + ) + + client.set_branch_protection( + owner=options.owner, + repo=options.repo, + branch=options.default_branch, + ) + + if options.dry_run: + print("[dry-run] bootstrap complete") diff --git a/src/platform_bootstrap/cli.py b/src/platform_bootstrap/cli.py new file mode 100644 index 0000000..38c6e96 --- /dev/null +++ b/src/platform_bootstrap/cli.py @@ -0,0 +1,47 @@ +from __future__ import annotations + +import argparse +import sys + +from platform_bootstrap.bootstrap import bootstrap_repo + + +def build_parser() -> argparse.ArgumentParser: + parser = argparse.ArgumentParser( + prog="platform-bootstrap", + description="Bootstrap a GitHub repository with baseline files and policies.", + ) + subparsers = parser.add_subparsers(dest="command", required=True) + + bootstrap_parser = subparsers.add_parser( + "bootstrap", help="Apply baseline bootstrap to a repository" + ) + bootstrap_parser.add_argument("--owner", required=True) + bootstrap_parser.add_argument("--repo", required=True) + bootstrap_parser.add_argument("--default-branch", default="main") + bootstrap_parser.add_argument("--codeowners", required=True) + bootstrap_parser.add_argument("--dry-run", action="store_true") + + return parser + + +def main(argv: list[str] | None = None) -> int: + parser = build_parser() + args = parser.parse_args(argv) + + if args.command == "bootstrap": + bootstrap_repo( + owner=args.owner, + repo=args.repo, + default_branch=args.default_branch, + codeowners=args.codeowners, + dry_run=args.dry_run, + ) + return 0 + + parser.print_help() + return 2 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/src/platform_bootstrap/github_client.py b/src/platform_bootstrap/github_client.py new file mode 100644 index 0000000..7c45f52 --- /dev/null +++ b/src/platform_bootstrap/github_client.py @@ -0,0 +1,74 @@ +from __future__ import annotations + +from dataclasses import dataclass +import os +from typing import Optional + +import requests + + +@dataclass +class GitHubClient: + token: Optional[str] + dry_run: bool = False + base_url: str = "https://api.github.com" + + @classmethod + def from_env(cls, *, dry_run: bool = False) -> "GitHubClient": + token = os.getenv("GITHUB_TOKEN") + if not token and not dry_run: + raise ValueError("GITHUB_TOKEN is required for non-dry-run operations.") + if not token and dry_run: + print("[dry-run] GITHUB_TOKEN not set; using unauthenticated mode.") + return cls(token=token, dry_run=dry_run) + + def _headers(self) -> dict: + headers = {"Accept": "application/vnd.github+json"} + if self.token: + headers["Authorization"] = f"Bearer {self.token}" + return headers + + def ensure_repo(self, owner: str, repo: str) -> bool: + if self.dry_run: + print(f"[dry-run] ensure repo exists: {owner}/{repo}") + return True + url = f"{self.base_url}/repos/{owner}/{repo}" + response = requests.get(url, headers=self._headers(), timeout=10) + if response.status_code == 200: + return True + if response.status_code == 404: + return False + raise RuntimeError( + f"GitHub API error ({response.status_code}): {response.text}" + ) + + def upsert_file( + self, + *, + owner: str, + repo: str, + path: str, + content: str, + message: str, + branch: str, + ) -> None: + if self.dry_run: + print( + "[dry-run] upsert file: " + f"{owner}/{repo}:{path} (branch={branch})" + ) + return + raise NotImplementedError( + "TODO: Implement GitHub contents API call for upserting files." + ) + + def set_branch_protection(self, *, owner: str, repo: str, branch: str) -> None: + if self.dry_run: + print( + "[dry-run] set branch protection: " + f"{owner}/{repo}:{branch}" + ) + return + raise NotImplementedError( + "TODO: Implement branch protection API call." + ) diff --git a/tests/test_smoke.py b/tests/test_smoke.py new file mode 100644 index 0000000..5c67d9a --- /dev/null +++ b/tests/test_smoke.py @@ -0,0 +1,11 @@ +from platform_bootstrap.bootstrap import bootstrap_repo + + +def test_bootstrap_dry_run_smoke() -> None: + bootstrap_repo( + owner="octo-org", + repo="demo", + default_branch="main", + codeowners="@octo-org/platform", + dry_run=True, + ) From b47885b71363e599af06391d6e1fa8b0b5906b06 Mon Sep 17 00:00:00 2001 From: Gaurav Joshi Date: Thu, 12 Feb 2026 11:04:04 -0700 Subject: [PATCH 2/2] Install test deps in CI and add requirements Add a requirements.txt with pytest>=7.0 and update the GitHub Actions CI workflow to install dependencies via `pip install -r requirements.txt` (in addition to the editable install). Ensures pytest is available and test dependencies are centralized for the CI job. --- .github/workflows/ci.yml | 4 +++- requirements.txt | 1 + 2 files changed, 4 insertions(+), 1 deletion(-) create mode 100644 requirements.txt diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 9fb3e21..b70be96 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -17,6 +17,8 @@ jobs: with: python-version: ${{ matrix.python-version }} - name: Install - run: python -m pip install -e . + run: | + python -m pip install -e . + python -m pip install -r requirements.txt - name: Test run: pytest diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..b197d32 --- /dev/null +++ b/requirements.txt @@ -0,0 +1 @@ +pytest>=7.0