Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
42 commits
Select commit Hold shift + click to select a range
b199631
feat: swap to making a new feature branch in demo rather than working…
56kyle Nov 5, 2025
8df514f
chore: small niceties for commit message and not trying a redundant b…
56kyle Nov 5, 2025
2d1d15b
fix: swap is_ancestor to use its own error handling due to git merge-…
56kyle Nov 6, 2025
f99c0b8
fix: remove accidentally added import
56kyle Nov 6, 2025
0ff3bcd
fix: add a few workarounds trying to get POC branching going before r…
56kyle Nov 6, 2025
f5aebbe
fix: ensure that pushing a new branch for the first time works
56kyle Nov 6, 2025
d746b9a
feat: rebase branching pattern on develop
56kyle Nov 5, 2025
dc323d2
chore: small niceties for commit message and not trying a redundant b…
56kyle Nov 5, 2025
a04fa6c
fix: swap is_ancestor to use its own error handling due to git merge-…
56kyle Nov 6, 2025
85e2d50
fix: remove accidentally added import
56kyle Nov 6, 2025
791cdcf
fix: add a few workarounds trying to get POC branching going before r…
56kyle Nov 6, 2025
456aaaf
fix: ensure that pushing a new branch for the first time works
56kyle Nov 6, 2025
589dfcd
refactor: make git related commands more accurate
56kyle Nov 18, 2025
3a1a71a
Merge remote-tracking branch 'origin/feature/improve-demo-branching-p…
56kyle Nov 18, 2025
317c7a5
chore: update logo
56kyle Nov 18, 2025
94fd2b5
feat: add PEP 723 syntax installs
56kyle Nov 18, 2025
9816734
refactor: break apart util function for validating branch history and…
56kyle Nov 18, 2025
6b02056
refactor: move constants to module level in places
56kyle Nov 18, 2025
35b918b
feat: add a check to the setup-release script in generated project al…
56kyle Nov 18, 2025
e25b228
feat: add logic for rolling back release creation
56kyle Nov 19, 2025
18a10d8
feat: update logic in release rollback to not get hung up on faulty c…
56kyle Nov 19, 2025
426a90a
feat: add a ton of half finished logic for release handling that will…
56kyle Nov 24, 2025
b8c4885
fix: remove faulty syntax
56kyle Nov 24, 2025
0aece59
feat: improve logic for creating demo releases in github
56kyle Nov 24, 2025
c6d8074
feat: update demo handling to create new feature branches when target…
56kyle Nov 28, 2025
a42e68b
fix: replace broken import in scripts
56kyle Nov 28, 2025
28dab17
feat: add initial attempt at a template level github action to sync d…
56kyle Nov 28, 2025
a424626
fix: remove unintended broken snippet
56kyle Nov 28, 2025
0aebe5c
fix: add interop layer between older noxfile methods and newer env va…
56kyle Nov 28, 2025
b20c803
chore: remove accidentally left unused snippet
56kyle Nov 28, 2025
dad65b3
fix: remove invalid pass through attempts of text=true throughout scr…
56kyle Nov 28, 2025
1b56f02
fix: remove old code from update-demo that is no longer compatible
56kyle Nov 28, 2025
c3f1a3c
refactor: rename script function to reflect usage only occurring in demo
56kyle Nov 28, 2025
964ec8e
feat: remove unused merge commit check
56kyle Nov 28, 2025
0e8f4e7
feat: alter sync demo process to use ephemeral github commits for gen…
56kyle Nov 28, 2025
b2f748b
feat: add on automated demo PR creation from feature branches into de…
56kyle Nov 28, 2025
d1e939f
fix: update key used to find commit info in cruft json
56kyle Nov 28, 2025
8bca3af
feat: temporarily remove the body generation of the feature to develo…
56kyle Nov 28, 2025
39b6f07
fix: alter kwarg chaining in hopes of fixing error
56kyle Nov 28, 2025
eca180f
fix: add working-directory kwarg to sync-demo so that the nox command…
56kyle Nov 28, 2025
c2cb3c5
fix: replace matrix values in sync-demo to account for previous simpl…
56kyle Nov 28, 2025
eda488c
fix: add old env var for project cache to sync-demos.yml for the time…
56kyle Nov 28, 2025
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
22 changes: 17 additions & 5 deletions .env
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,22 @@
# Users can override these by creating a .env.local file (not committed to git)

# App author name used for cache directory paths
COOKIECUTTER_ROBUST_PYTHON_APP_AUTHOR=robust-python
COOKIECUTTER_ROBUST_PYTHON__DEMOS_CACHE_FOLDER=""

# Main branch name (typically used for stable releases)
COOKIECUTTER_ROBUST_PYTHON_MAIN_BRANCH=main
COOKIECUTTER_ROBUST_PYTHON__APP_NAME="cookiecutter-robust-python"
COOKIECUTTER_ROBUST_PYTHON__APP_AUTHOR="robust-python"
COOKIECUTTER_ROBUST_PYTHON__REMOTE="origin"
COOKIECUTTER_ROBUST_PYTHON__MAIN_BRANCH="main"
COOKIECUTTER_ROBUST_PYTHON__DEVELOP_BRANCH="develop"

# Development branch name (where feature development occurs)
COOKIECUTTER_ROBUST_PYTHON_DEVELOP_BRANCH=develop
ROBUST_PYTHON_DEMO__APP_NAME="robust-python-demo"
ROBUST_PYTHON_DEMO__APP_AUTHOR="robust-python"
ROBUST_PYTHON_DEMO__REMOTE="origin"
ROBUST_PYTHON_DEMO__MAIN_BRANCH="main"
ROBUST_PYTHON_DEMO__DEVELOP_BRANCH="develop"

ROBUST_MATURIN_DEMO__APP_NAME="robust-maturin-demo"
ROBUST_MATURIN_DEMO__APP_AUTHOR="robust-python"
ROBUST_MATURIN_DEMO__REMOTE="origin"
ROBUST_MATURIN_DEMO__MAIN_BRANCH="main"
ROBUST_MATURIN_DEMO__DEVELOP_BRANCH="develop"
48 changes: 48 additions & 0 deletions .github/workflows/sync-demos.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
name: sync-demo.yml
on:
pull_request:
branches:
- develop

env:
COOKIECUTTER_ROBUST_PYTHON_PROJECT_DEMOS_FOLDER: ${{ github.workspace }}
COOKIECUTTER_ROBUST_PYTHON__DEMOS_CACHE_FOLDER: ${{ github.workspace }}
COOKIECUTTER_ROBUST_PYTHON__APP_AUTHOR: ${{ github.repository_owner }}
ROBUST_PYTHON_DEMO__APP_AUTHOR: ${{ github.repository_owner }}
ROBUST_MATURIN_DEMO__APP_AUTHOR: ${{ github.repository_owner }}

jobs:
update-demo:
name: Update Demo
runs-on: ubuntu-latest

strategy:
matrix:
demo_name:
- "robust-python-demo"
- "robust-maturin-demo"
steps:
- name: Checkout Template
uses: actions/checkout@v4
with:
repository: ${{ github.repository }}
path: cookiecutter-robust-python

- name: Checkout Demo
uses: actions/checkout@v4
with:
repository: "${{ github.repository_owner }}/${{ matrix.demo_name }}"
path: ${{ matrix.demo_name }}
ref: develop

- name: Set up uv
uses: astral-sh/setup-uv@v6

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

- name: Update Demo
working-directory: "${{ github.workspace }}/cookiecutter-robust-python"
run: "uvx nox -s 'update-demo(${{ matrix.demo_name }})' -- --branch-override ${{ github.head_ref }}"
Binary file modified docs/_static/cookiecutter-robust-python-banner.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
55 changes: 51 additions & 4 deletions noxfile.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,10 @@

import os
import shutil
from dataclasses import asdict
from dataclasses import dataclass
from pathlib import Path
from typing import Any

import nox
import platformdirs
Expand Down Expand Up @@ -66,6 +69,41 @@
)


@dataclass
class RepoMetadata:
"""Metadata for a given repo."""
app_name: str
app_author: str
remote: str
main_branch: str
develop_branch: str


TEMPLATE: RepoMetadata = RepoMetadata(
app_name=os.getenv("COOKIECUTTER_ROBUST_PYTHON__APP_NAME"),
app_author=os.getenv("COOKIECUTTER_ROBUST_PYTHON__APP_AUTHOR"),
remote=os.getenv("COOKIECUTTER_ROBUST_PYTHON__REMOTE"),
main_branch=os.getenv("COOKIECUTTER_ROBUST_PYTHON__MAIN_BRANCH"),
develop_branch=os.getenv("COOKIECUTTER_ROBUST_PYTHON__DEVELOP_BRANCH")
)

PYTHON_DEMO: RepoMetadata = RepoMetadata(
app_name=os.getenv("ROBUST_PYTHON_DEMO__APP_NAME"),
app_author=os.getenv("ROBUST_PYTHON_DEMO__APP_AUTHOR"),
remote=os.getenv("ROBUST_PYTHON_DEMO__REMOTE"),
main_branch=os.getenv("ROBUST_PYTHON_DEMO__MAIN_BRANCH"),
develop_branch=os.getenv("ROBUST_PYTHON_DEMO__DEVELOP_BRANCH")
)

MATURIN_DEMO: RepoMetadata = RepoMetadata(
app_name=os.getenv("ROBUST_MATURIN_DEMO__APP_NAME"),
app_author=os.getenv("ROBUST_MATURIN_DEMO__APP_AUTHOR"),
remote=os.getenv("ROBUST_MATURIN_DEMO__REMOTE"),
main_branch=os.getenv("ROBUST_MATURIN_DEMO__MAIN_BRANCH"),
develop_branch=os.getenv("ROBUST_MATURIN_DEMO__DEVELOP_BRANCH")
)


@nox.session(python=DEFAULT_TEMPLATE_PYTHON_VERSION, name="generate-demo")
def generate_demo(session: Session) -> None:
"""Generates a project demo using the cookiecutter-robust-python template."""
Expand Down Expand Up @@ -143,17 +181,26 @@ def test(session: Session) -> None:
session.run("pytest", "tests")


@nox.parametrize(arg_names="add_rust_extension", arg_values_list=[False, True], ids=["no-rust", "rust"])
@nox.parametrize(
arg_names="demo",
arg_values_list=[PYTHON_DEMO, MATURIN_DEMO],
ids=["robust-python-demo", "robust-maturin-demo"]
)
@nox.session(python=DEFAULT_TEMPLATE_PYTHON_VERSION, name="update-demo")
def update_demo(session: Session, add_rust_extension: bool) -> None:
def update_demo(session: Session, demo: RepoMetadata) -> None:
session.log("Installing script dependencies for updating generated project demos...")
session.install("cookiecutter", "cruft", "platformdirs", "loguru", "python-dotenv", "typer")

session.log("Updating generated project demos...")
args: list[str] = [*UPDATE_DEMO_OPTIONS]
if add_rust_extension:
if "maturin" in demo.app_name:
args.append("--add-rust-extension")
session.run("python", UPDATE_DEMO_SCRIPT, *args)

if session.posargs:
args.extend(session.posargs)

demo_env: dict[str, Any] = {f"ROBUST_DEMO__{key.upper()}": value for key, value in asdict(demo).items()}
session.run("python", UPDATE_DEMO_SCRIPT, *args, env=demo_env)


@nox.session(python=False, name="release-template")
Expand Down
10 changes: 10 additions & 0 deletions scripts/generate-demo.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,14 @@
# /// script
# requires-python = ">=3.10"
# dependencies = [
# "cookiecutter",
# "cruft",
# "python-dotenv",
# "typer",
# ]
# ///
"""Python script for generating a demo project."""

import sys
from pathlib import Path
from typing import Annotated
Expand Down
23 changes: 17 additions & 6 deletions scripts/lint-from-demo.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,14 @@
# /// script
# requires-python = ">=3.10"
# dependencies = [
# "cookiecutter",
# "cruft",
# "python-dotenv",
# "retrocookie",
# "typer",
# ]
# ///

import os
from pathlib import Path
from typing import Annotated
Expand All @@ -6,10 +17,11 @@
import typer
from retrocookie.core import retrocookie

from util import DEMO
from util import git
from util import FolderOption
from util import in_new_demo
from util import require_clean_and_up_to_date_repo
from util import require_clean_and_up_to_date_demo_repo


# These still may need linted, but retrocookie shouldn't be used on them
Expand All @@ -29,23 +41,22 @@ def lint_from_demo(
no_cache: Annotated[bool, typer.Option("--no-cache", "-n")] = False
) -> None:
"""Runs precommit in a generated project and matches the template to the results."""
develop_branch: str = os.getenv("COOKIECUTTER_ROBUST_PYTHON_DEVELOP_BRANCH", "develop")
with in_new_demo(
demos_cache_folder=demos_cache_folder,
add_rust_extension=add_rust_extension,
no_cache=no_cache
) as demo_path:
require_clean_and_up_to_date_repo()
git("checkout", develop_branch)
require_clean_and_up_to_date_demo_repo(demo_path=demo_path)
git("checkout", DEMO.develop_branch)
git("branch", "-D", "temp/lint-from-demo", ignore_error=True)
git("checkout", "-b", "temp/lint-from-demo", develop_branch)
git("checkout", "-b", "temp/lint-from-demo", DEMO.develop_branch)
pre_commit.main.main(["run", "--all-files", "--show-diff-on-failure"])

for path in IGNORED_FILES:
git("checkout", "HEAD", "--", path)
git("add", ".")
git("commit", "-m", "meta: lint-from-demo", "--no-verify")
retrocookie(instance_path=demo_path, commits=[f"{develop_branch}..temp/lint-from-demo"])
retrocookie(instance_path=demo_path, commits=[f"{DEMO.develop_branch}..temp/lint-from-demo"])


if __name__ == '__main__':
Expand Down
96 changes: 96 additions & 0 deletions scripts/release-demo.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
# /// script
# requires-python = ">=3.10"
# dependencies = [
# "cookiecutter",
# "cruft",
# "python-dotenv",
# "typer",
# ]
# ///
import itertools
import subprocess
import tempfile
from pathlib import Path
from typing import Annotated

import typer
from cookiecutter.utils import work_in
from loguru import logger

from util import get_current_branch
from util import get_demo_name
from util import gh
from util import git
from util import nox
from util import require_clean_and_up_to_date_demo_repo
from util import FolderOption
from util import DEMO


cli: typer.Typer = typer.Typer()


@cli.callback(invoke_without_command=True)
def release_demo(
demos_cache_folder: Annotated[Path, FolderOption("--demos-cache-folder", "-c")],
add_rust_extension: Annotated[bool, typer.Option("--add-rust-extension", "-r")] = False
) -> None:
"""Creates a release of the demo's current develop branch if changes exist."""
demo_name: str = get_demo_name(add_rust_extension=add_rust_extension)
demo_path: Path = demos_cache_folder / demo_name

with work_in(demo_path):
require_clean_and_up_to_date_demo_repo(demo_path)
git("checkout", DEMO.develop_branch)
try:
nox("setup-release", "--", "MINOR")
logger.success(f"Successfully created release {demo_name}")
gh("repo", "set-default", DEMO.app_author, DEMO.app_name)

except subprocess.CalledProcessError as error:
logger.warning(f"Failed to setup release: {error}")
_rollback_failed_release()
raise error

git("push", "-u")
_create_demo_pr()


def _rollback_failed_release() -> None:
"""Returns the demo repo back to the state it was prior to the release attempt."""
starting_demo_branch: str = get_current_branch()

if starting_demo_branch != "develop":
git("checkout", DEMO.develop_branch)

git("checkout", ".")
git("branch", "-D", starting_demo_branch)


def _create_demo_pr() -> None:
"""Creates a pull request to merge the demo's feature branch into ."""
current_branch: str = get_current_branch()
if not current_branch.startswith("release/"):
raise ValueError("Not in a release branch, canceling PR creation.")
title: str = current_branch.capitalize()
release_notes: str = __get_demo_release_notes()

pr_kwargs: dict[str, str] = {
"--title": title,
"--body": release_notes,
"--assignee": "@me",
"--base": "main",
}
command: list[str] = ["gh", "pr", "create", *itertools.chain(pr_kwargs.items())]
subprocess.run(command, check=True)


def __get_demo_release_notes() -> str:
"""Returns the release notes for the demo."""
temp_folder: Path = Path(tempfile.mkdtemp()).resolve()
notes_path: Path = temp_folder / "body.md"
command: list[str] = ["uv", "run", "./scripts/get-release-notes.py", notes_path]
subprocess.run(command, check=True)

notes_contents: str = notes_path.read_text()
return notes_contents
Loading
Loading