Skip to content

Commit d7b2c18

Browse files
authored
Merge pull request #65 from robust-python/feature/improve-demo-branching-pattern
Feature/improve demo branching pattern
2 parents 7dad9f1 + 86b25ef commit d7b2c18

File tree

11 files changed

+515
-65
lines changed

11 files changed

+515
-65
lines changed

.env

Lines changed: 17 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,22 @@
22
# Users can override these by creating a .env.local file (not committed to git)
33

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

7-
# Main branch name (typically used for stable releases)
8-
COOKIECUTTER_ROBUST_PYTHON_MAIN_BRANCH=main
7+
COOKIECUTTER_ROBUST_PYTHON__APP_NAME="cookiecutter-robust-python"
8+
COOKIECUTTER_ROBUST_PYTHON__APP_AUTHOR="robust-python"
9+
COOKIECUTTER_ROBUST_PYTHON__REMOTE="origin"
10+
COOKIECUTTER_ROBUST_PYTHON__MAIN_BRANCH="main"
11+
COOKIECUTTER_ROBUST_PYTHON__DEVELOP_BRANCH="develop"
912

10-
# Development branch name (where feature development occurs)
11-
COOKIECUTTER_ROBUST_PYTHON_DEVELOP_BRANCH=develop
13+
ROBUST_PYTHON_DEMO__APP_NAME="robust-python-demo"
14+
ROBUST_PYTHON_DEMO__APP_AUTHOR="robust-python"
15+
ROBUST_PYTHON_DEMO__REMOTE="origin"
16+
ROBUST_PYTHON_DEMO__MAIN_BRANCH="main"
17+
ROBUST_PYTHON_DEMO__DEVELOP_BRANCH="develop"
18+
19+
ROBUST_MATURIN_DEMO__APP_NAME="robust-maturin-demo"
20+
ROBUST_MATURIN_DEMO__APP_AUTHOR="robust-python"
21+
ROBUST_MATURIN_DEMO__REMOTE="origin"
22+
ROBUST_MATURIN_DEMO__MAIN_BRANCH="main"
23+
ROBUST_MATURIN_DEMO__DEVELOP_BRANCH="develop"

.github/workflows/sync-demos.yml

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
name: sync-demo.yml
2+
on:
3+
pull_request:
4+
branches:
5+
- develop
6+
7+
env:
8+
COOKIECUTTER_ROBUST_PYTHON_PROJECT_DEMOS_FOLDER: ${{ github.workspace }}
9+
COOKIECUTTER_ROBUST_PYTHON__DEMOS_CACHE_FOLDER: ${{ github.workspace }}
10+
COOKIECUTTER_ROBUST_PYTHON__APP_AUTHOR: ${{ github.repository_owner }}
11+
ROBUST_PYTHON_DEMO__APP_AUTHOR: ${{ github.repository_owner }}
12+
ROBUST_MATURIN_DEMO__APP_AUTHOR: ${{ github.repository_owner }}
13+
14+
jobs:
15+
update-demo:
16+
name: Update Demo
17+
runs-on: ubuntu-latest
18+
19+
strategy:
20+
matrix:
21+
demo_name:
22+
- "robust-python-demo"
23+
- "robust-maturin-demo"
24+
steps:
25+
- name: Checkout Template
26+
uses: actions/checkout@v4
27+
with:
28+
repository: ${{ github.repository }}
29+
path: cookiecutter-robust-python
30+
31+
- name: Checkout Demo
32+
uses: actions/checkout@v4
33+
with:
34+
repository: "${{ github.repository_owner }}/${{ matrix.demo_name }}"
35+
path: ${{ matrix.demo_name }}
36+
ref: develop
37+
38+
- name: Set up uv
39+
uses: astral-sh/setup-uv@v6
40+
41+
- name: Set up Python
42+
uses: actions/setup-python@v5
43+
with:
44+
python-version: ${{ matrix.python-version }}
45+
46+
- name: Update Demo
47+
working-directory: "${{ github.workspace }}/cookiecutter-robust-python"
48+
run: "uvx nox -s 'update-demo(${{ matrix.demo_name }})' -- --branch-override ${{ github.head_ref }}"
-290 Bytes
Loading

noxfile.py

Lines changed: 51 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,10 @@
66

77
import os
88
import shutil
9+
from dataclasses import asdict
10+
from dataclasses import dataclass
911
from pathlib import Path
12+
from typing import Any
1013

1114
import nox
1215
import platformdirs
@@ -66,6 +69,41 @@
6669
)
6770

6871

72+
@dataclass
73+
class RepoMetadata:
74+
"""Metadata for a given repo."""
75+
app_name: str
76+
app_author: str
77+
remote: str
78+
main_branch: str
79+
develop_branch: str
80+
81+
82+
TEMPLATE: RepoMetadata = RepoMetadata(
83+
app_name=os.getenv("COOKIECUTTER_ROBUST_PYTHON__APP_NAME"),
84+
app_author=os.getenv("COOKIECUTTER_ROBUST_PYTHON__APP_AUTHOR"),
85+
remote=os.getenv("COOKIECUTTER_ROBUST_PYTHON__REMOTE"),
86+
main_branch=os.getenv("COOKIECUTTER_ROBUST_PYTHON__MAIN_BRANCH"),
87+
develop_branch=os.getenv("COOKIECUTTER_ROBUST_PYTHON__DEVELOP_BRANCH")
88+
)
89+
90+
PYTHON_DEMO: RepoMetadata = RepoMetadata(
91+
app_name=os.getenv("ROBUST_PYTHON_DEMO__APP_NAME"),
92+
app_author=os.getenv("ROBUST_PYTHON_DEMO__APP_AUTHOR"),
93+
remote=os.getenv("ROBUST_PYTHON_DEMO__REMOTE"),
94+
main_branch=os.getenv("ROBUST_PYTHON_DEMO__MAIN_BRANCH"),
95+
develop_branch=os.getenv("ROBUST_PYTHON_DEMO__DEVELOP_BRANCH")
96+
)
97+
98+
MATURIN_DEMO: RepoMetadata = RepoMetadata(
99+
app_name=os.getenv("ROBUST_MATURIN_DEMO__APP_NAME"),
100+
app_author=os.getenv("ROBUST_MATURIN_DEMO__APP_AUTHOR"),
101+
remote=os.getenv("ROBUST_MATURIN_DEMO__REMOTE"),
102+
main_branch=os.getenv("ROBUST_MATURIN_DEMO__MAIN_BRANCH"),
103+
develop_branch=os.getenv("ROBUST_MATURIN_DEMO__DEVELOP_BRANCH")
104+
)
105+
106+
69107
@nox.session(python=DEFAULT_TEMPLATE_PYTHON_VERSION, name="generate-demo")
70108
def generate_demo(session: Session) -> None:
71109
"""Generates a project demo using the cookiecutter-robust-python template."""
@@ -143,17 +181,26 @@ def test(session: Session) -> None:
143181
session.run("pytest", "tests")
144182

145183

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

152194
session.log("Updating generated project demos...")
153195
args: list[str] = [*UPDATE_DEMO_OPTIONS]
154-
if add_rust_extension:
196+
if "maturin" in demo.app_name:
155197
args.append("--add-rust-extension")
156-
session.run("python", UPDATE_DEMO_SCRIPT, *args)
198+
199+
if session.posargs:
200+
args.extend(session.posargs)
201+
202+
demo_env: dict[str, Any] = {f"ROBUST_DEMO__{key.upper()}": value for key, value in asdict(demo).items()}
203+
session.run("python", UPDATE_DEMO_SCRIPT, *args, env=demo_env)
157204

158205

159206
@nox.session(python=False, name="release-template")

scripts/generate-demo.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,14 @@
1+
# /// script
2+
# requires-python = ">=3.10"
3+
# dependencies = [
4+
# "cookiecutter",
5+
# "cruft",
6+
# "python-dotenv",
7+
# "typer",
8+
# ]
9+
# ///
110
"""Python script for generating a demo project."""
11+
212
import sys
313
from pathlib import Path
414
from typing import Annotated

scripts/lint-from-demo.py

Lines changed: 17 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,14 @@
1+
# /// script
2+
# requires-python = ">=3.10"
3+
# dependencies = [
4+
# "cookiecutter",
5+
# "cruft",
6+
# "python-dotenv",
7+
# "retrocookie",
8+
# "typer",
9+
# ]
10+
# ///
11+
112
import os
213
from pathlib import Path
314
from typing import Annotated
@@ -6,10 +17,11 @@
617
import typer
718
from retrocookie.core import retrocookie
819

20+
from util import DEMO
921
from util import git
1022
from util import FolderOption
1123
from util import in_new_demo
12-
from util import require_clean_and_up_to_date_repo
24+
from util import require_clean_and_up_to_date_demo_repo
1325

1426

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

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

5061

5162
if __name__ == '__main__':

scripts/release-demo.py

Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
# /// script
2+
# requires-python = ">=3.10"
3+
# dependencies = [
4+
# "cookiecutter",
5+
# "cruft",
6+
# "python-dotenv",
7+
# "typer",
8+
# ]
9+
# ///
10+
import itertools
11+
import subprocess
12+
import tempfile
13+
from pathlib import Path
14+
from typing import Annotated
15+
16+
import typer
17+
from cookiecutter.utils import work_in
18+
from loguru import logger
19+
20+
from util import get_current_branch
21+
from util import get_demo_name
22+
from util import gh
23+
from util import git
24+
from util import nox
25+
from util import require_clean_and_up_to_date_demo_repo
26+
from util import FolderOption
27+
from util import DEMO
28+
29+
30+
cli: typer.Typer = typer.Typer()
31+
32+
33+
@cli.callback(invoke_without_command=True)
34+
def release_demo(
35+
demos_cache_folder: Annotated[Path, FolderOption("--demos-cache-folder", "-c")],
36+
add_rust_extension: Annotated[bool, typer.Option("--add-rust-extension", "-r")] = False
37+
) -> None:
38+
"""Creates a release of the demo's current develop branch if changes exist."""
39+
demo_name: str = get_demo_name(add_rust_extension=add_rust_extension)
40+
demo_path: Path = demos_cache_folder / demo_name
41+
42+
with work_in(demo_path):
43+
require_clean_and_up_to_date_demo_repo(demo_path)
44+
git("checkout", DEMO.develop_branch)
45+
try:
46+
nox("setup-release", "--", "MINOR")
47+
logger.success(f"Successfully created release {demo_name}")
48+
gh("repo", "set-default", DEMO.app_author, DEMO.app_name)
49+
50+
except subprocess.CalledProcessError as error:
51+
logger.warning(f"Failed to setup release: {error}")
52+
_rollback_failed_release()
53+
raise error
54+
55+
git("push", "-u")
56+
_create_demo_pr()
57+
58+
59+
def _rollback_failed_release() -> None:
60+
"""Returns the demo repo back to the state it was prior to the release attempt."""
61+
starting_demo_branch: str = get_current_branch()
62+
63+
if starting_demo_branch != "develop":
64+
git("checkout", DEMO.develop_branch)
65+
66+
git("checkout", ".")
67+
git("branch", "-D", starting_demo_branch)
68+
69+
70+
def _create_demo_pr() -> None:
71+
"""Creates a pull request to merge the demo's feature branch into ."""
72+
current_branch: str = get_current_branch()
73+
if not current_branch.startswith("release/"):
74+
raise ValueError("Not in a release branch, canceling PR creation.")
75+
title: str = current_branch.capitalize()
76+
release_notes: str = __get_demo_release_notes()
77+
78+
pr_kwargs: dict[str, str] = {
79+
"--title": title,
80+
"--body": release_notes,
81+
"--assignee": "@me",
82+
"--base": "main",
83+
}
84+
command: list[str] = ["gh", "pr", "create", *itertools.chain(pr_kwargs.items())]
85+
subprocess.run(command, check=True)
86+
87+
88+
def __get_demo_release_notes() -> str:
89+
"""Returns the release notes for the demo."""
90+
temp_folder: Path = Path(tempfile.mkdtemp()).resolve()
91+
notes_path: Path = temp_folder / "body.md"
92+
command: list[str] = ["uv", "run", "./scripts/get-release-notes.py", notes_path]
93+
subprocess.run(command, check=True)
94+
95+
notes_contents: str = notes_path.read_text()
96+
return notes_contents

0 commit comments

Comments
 (0)