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
24 changes: 24 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
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 .
python -m pip install -r requirements.txt
- name: Test
run: pytest
17 changes: 17 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -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
21 changes: 21 additions & 0 deletions LICENSE
Original file line number Diff line number Diff line change
@@ -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.
112 changes: 111 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1 +1,111 @@
# platform-bootstrap
# 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
25 changes: 25 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
@@ -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"]
1 change: 1 addition & 0 deletions requirements.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
pytest>=7.0
4 changes: 4 additions & 0 deletions src/platform_bootstrap/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
"""Platform bootstrap package."""

__all__ = ["__version__"]
__version__ = "0.1.0"
129 changes: 129 additions & 0 deletions src/platform_bootstrap/bootstrap.py
Original file line number Diff line number Diff line change
@@ -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")
47 changes: 47 additions & 0 deletions src/platform_bootstrap/cli.py
Original file line number Diff line number Diff line change
@@ -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())
Loading