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
53 changes: 53 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,59 @@ concurrency:
cancel-in-progress: true

jobs:
lint:
name: lint (ruff + mypy)
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4

- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: "3.12"

- name: Install package (dev extras)
run: python -m pip install -e ".[dev]"

- name: Ruff check
run: python -m ruff check src tests

- name: Ruff format (check only)
run: python -m ruff format --check src tests

- name: Mypy
run: python -m mypy

build:
name: build sdist/wheel
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4

- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: "3.12"

- name: Install build tooling
run: python -m pip install -e ".[dev]"

- name: Build distributions
run: python -m build

- name: Twine check
run: python -m twine check dist/*

- name: Upload dist artifacts
uses: actions/upload-artifact@v4
with:
name: python-package-distributions
path: dist/
if-no-files-found: error
retention-days: 7

test:
name: pytest (py${{ matrix.python-version }}, ${{ matrix.os }})
runs-on: ${{ matrix.os }}
Expand Down
147 changes: 147 additions & 0 deletions .github/workflows/publish.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,147 @@
# Publish cppboot to PyPI when a GitHub Release is published.
#
# Idiomatic flow:
# 1. Bump src/cppboot/_version.py (and CHANGELOG)
# 2. Merge to main
# 3. Create a GitHub Release with tag vX.Y.Z (must match package version)
# 4. This workflow builds sdist/wheel and uploads via Trusted Publishing (OIDC)
#
# Prerequisites (one-time, before first real publish — not done by this PR):
# - PyPI project "cppboot" with Trusted Publisher:
# Owner: bluesentinelsec, Repository: cppboot,
# Workflow: publish.yml, Environment: pypi
# - Optional TestPyPI trusted publisher with Environment: testpypi
# - GitHub Environments "pypi" / "testpypi" (optional protection rules)
#
# This workflow does not run until a Release is published (or manual dispatch).
# Do not publish until you are intentionally ready for PyPI.
name: Publish

on:
release:
types: [published]
workflow_dispatch:
inputs:
target:
description: "Publish target (use testpypi until ready for real PyPI)"
type: choice
options:
- testpypi
- pypi
default: testpypi

permissions:
contents: read

concurrency:
group: publish-${{ github.event_name }}-${{ github.ref }}
cancel-in-progress: false

jobs:
build:
name: Build distributions
runs-on: ubuntu-latest
outputs:
package_version: ${{ steps.meta.outputs.package_version }}
steps:
- name: Checkout
uses: actions/checkout@v4

- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: "3.12"

- name: Install build tooling
run: python -m pip install -e ".[dev]"

- name: Read package version
id: meta
run: |
set -euo pipefail
version="$(python -c 'from cppboot._version import __version__; print(__version__)')"
echo "package_version=${version}" >> "${GITHUB_OUTPUT}"
echo "Package version: ${version}"

- name: Verify release tag matches package version
if: github.event_name == 'release'
env:
TAG_NAME: ${{ github.event.release.tag_name }}
PACKAGE_VERSION: ${{ steps.meta.outputs.package_version }}
run: |
set -euo pipefail
tag="${TAG_NAME#v}"
tag="${tag#V}"
if [ "${tag}" != "${PACKAGE_VERSION}" ]; then
echo "Release tag '${TAG_NAME}' (normalized '${tag}') does not match"
echo "package version '${PACKAGE_VERSION}' from src/cppboot/_version.py"
echo "Bump _version.py and CHANGELOG before creating the GitHub Release."
exit 1
fi
echo "Tag and package version match: ${PACKAGE_VERSION}"

- name: Build sdist and wheel
run: python -m build

- name: Twine check
run: python -m twine check dist/*

- name: List artifacts
run: ls -la dist/

- name: Upload distributions
uses: actions/upload-artifact@v4
with:
name: python-package-distributions
path: dist/
if-no-files-found: error
retention-days: 14

publish-pypi:
name: Publish to PyPI
needs: build
if: >
github.event_name == 'release'
|| (github.event_name == 'workflow_dispatch' && inputs.target == 'pypi')
runs-on: ubuntu-latest
environment:
name: pypi
url: https://pypi.org/p/cppboot
permissions:
id-token: write # required for Trusted Publishing (OIDC)
steps:
- name: Download distributions
uses: actions/download-artifact@v4
with:
name: python-package-distributions
path: dist/

- name: Publish to PyPI
uses: pypa/gh-action-pypi-publish@release/v1
with:
packages-dir: dist/
print-hash: true

publish-testpypi:
name: Publish to TestPyPI
needs: build
if: github.event_name == 'workflow_dispatch' && inputs.target == 'testpypi'
runs-on: ubuntu-latest
environment:
name: testpypi
url: https://test.pypi.org/p/cppboot
permissions:
id-token: write
steps:
- name: Download distributions
uses: actions/download-artifact@v4
with:
name: python-package-distributions
path: dist/

- name: Publish to TestPyPI
uses: pypa/gh-action-pypi-publish@release/v1
with:
packages-dir: dist/
repository-url: https://test.pypi.org/legacy/
print-hash: true
19 changes: 13 additions & 6 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,20 +7,27 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

### Changed

- Split the monolithic generator into a `cppboot.generate` package (templates,
tooling, orchestration) while keeping `cppboot.generator` as a stable facade.
- Added package `LICENSE`, `CHANGELOG.md`, and `py.typed` for a PyPI-ready layout.

### Added

- **Publish** workflow: GitHub Release (`vX.Y.Z`) builds sdist/wheel and uploads
to PyPI via Trusted Publishing (OIDC). Manual `workflow_dispatch` can target
TestPyPI. Release tag must match `src/cppboot/_version.py`.
- Ruff + mypy in CI; distribution build + `twine check` job.
- Dev extras: `ruff`, `mypy`, `build`, `twine`.
- Opt-out flags for default-on behavior: `--no-git`, `--no-fmt`, `--no-community-docs`
(alongside existing `--no-vim`, `--no-ctags`, `--no-vscode`, `--no-github-actions`,
`--no-codespaces`).
- Offline license mode for deterministic tests (`ProjectOptions.offline_license`).
- pytest unit and integration suite with multi-Python CI.

### Changed

- Package version single-sourced from `src/cppboot/_version.py` (setuptools
dynamic version).
- Split the monolithic generator into a `cppboot.generate` package (templates,
tooling, orchestration) while keeping `cppboot.generator` as a stable facade.
- Added package `LICENSE`, `CHANGELOG.md`, and `py.typed` for a PyPI-ready layout.

## [0.1.0] - unreleased on PyPI

Initial public development line: CMake C++20 scaffolds, VERSION single source,
Expand Down
37 changes: 37 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,10 @@ Developers can start work with a clean `git status` and predictable formatting.
```bash
python3 -m pip install -e ".[dev]"
python3 -m pytest -q
python3 -m ruff check src tests
python3 -m ruff format --check src tests
python3 -m mypy
python3 -m build && python3 -m twine check dist/*
```

Tests cover CLI parsing, name/license logic, and filesystem generation with
Expand All @@ -92,6 +96,39 @@ Optional full C++ smoke (local only, not required for CI):
bash scripts/smoke.sh
```

### Releasing to PyPI (when ready)

Package version lives in **`src/cppboot/_version.py`** (single source; also used by
setuptools dynamic version).

1. Bump `__version__` in `_version.py` and update `CHANGELOG.md`
2. Merge to `main` (CI must be green)
3. Create a **GitHub Release** with tag **`vX.Y.Z`** matching that version
(GitHub UI: Releases → Draft a new release, or `gh release create vX.Y.Z`)
4. Workflow **Publish** (`.github/workflows/publish.yml`) runs on
`release: published`:
- Verifies tag `vX.Y.Z` == package version
- Builds sdist + wheel
- Uploads to **PyPI** via [Trusted Publishing](https://docs.pypi.org/trusted-publishers/)
(OIDC; no long-lived API token in the repo)

Manual dry-run target (does **not** publish to real PyPI unless you choose it):

```bash
gh workflow run publish.yml -f target=testpypi
```

**One-time setup before the first real publish** (not automated here):

1. Create the empty project on [PyPI](https://pypi.org/) (or claim `cppboot` on first upload)
2. Add a **Trusted Publisher**: owner `bluesentinelsec`, repo `cppboot`,
workflow `publish.yml`, environment `pypi`
3. Optionally configure TestPyPI the same way with environment `testpypi`
4. Create GitHub Environments `pypi` / `testpypi` (optional protection rules)

Until that setup is done, the publish job will fail at the upload step — which is
expected and safe while developing.

### Package layout

```text
Expand Down
48 changes: 47 additions & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"

[project]
name = "cppboot"
version = "0.1.0"
dynamic = ["version"]
description = "Bootstrap a professional-grade C++ project environment"
readme = "README.md"
requires-python = ">=3.10"
Expand All @@ -24,13 +24,18 @@ classifiers = [
"Programming Language :: Python :: 3.13",
"Topic :: Software Development :: Build Tools",
"Topic :: Software Development :: Code Generators",
"Typing :: Typed",
]
dependencies = []

[project.optional-dependencies]
dev = [
"pytest>=7.0",
"pytest-cov>=4.0",
"ruff>=0.9.0",
"mypy>=1.8.0",
"build>=1.2.0",
"twine>=5.0.0",
]

[project.scripts]
Expand All @@ -40,6 +45,10 @@ cppboot = "cppboot.cli:main"
Homepage = "https://github.com/bluesentinelsec/cppboot"
Repository = "https://github.com/bluesentinelsec/cppboot"
Issues = "https://github.com/bluesentinelsec/cppboot/issues"
Changelog = "https://github.com/bluesentinelsec/cppboot/blob/main/CHANGELOG.md"

[tool.setuptools.dynamic]
version = { attr = "cppboot._version.__version__" }

[tool.setuptools.packages.find]
where = ["src"]
Expand All @@ -60,3 +69,40 @@ markers = [
[tool.coverage.run]
source = ["cppboot"]
branch = true

[tool.ruff]
target-version = "py310"
line-length = 100
src = ["src", "tests"]

[tool.ruff.lint]
select = [
"E", # pycodestyle errors
"W", # pycodestyle warnings
"F", # pyflakes
"I", # isort
"B", # bugbear
"UP", # pyupgrade
"SIM", # simplify
]
ignore = [
"E501", # line length handled by formatter preference; templates are long
]

[tool.ruff.lint.per-file-ignores]
"tests/**" = ["B011"]
# Embedded Makefile/batch/CMake templates intentionally mix tabs and spaces.
"src/cppboot/generate/**" = ["E101"]

[tool.ruff.format]
quote-style = "double"
indent-style = "space"

[tool.mypy]
python_version = "3.10"
mypy_path = "src"
packages = ["cppboot"]
strict = true
warn_unused_configs = true
show_error_codes = true
pretty = true
3 changes: 1 addition & 2 deletions src/cppboot/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,9 @@

from __future__ import annotations

from cppboot._version import __version__
from cppboot.generator import GenerateResult, ProjectOptions, generate_project

__version__ = "0.1.0"

__all__ = [
"GenerateResult",
"ProjectOptions",
Expand Down
5 changes: 5 additions & 0 deletions src/cppboot/_version.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
"""Single source of truth for the installed package version."""

from __future__ import annotations

__version__ = "0.1.0"
Loading
Loading