From aa81a0e8dcc5042532d0c8d66b3f825effe640ec Mon Sep 17 00:00:00 2001 From: Andrey Tatarinov Date: Tue, 25 Nov 2025 23:30:59 +0400 Subject: [PATCH 1/3] feat: add support for multiple workflow templates per type, allowing suffix-based naming for generated workflows. --- CHANGELOG.md | 7 + README.md | 5 + pyproject.toml | 2 +- src/uv_workspace_codegen/main.py | 138 +++++++++++------- tests/test_main.py | 17 +-- tests/test_multi_template.py | 32 ++++ .../package.extra.template.yml | 7 + .../workflow-templates/package.template.yml | 7 + .../packages/pkg1/pyproject.toml | 5 + .../workspaces/multi_template/pyproject.toml | 5 + 10 files changed, 162 insertions(+), 63 deletions(-) create mode 100644 tests/test_multi_template.py create mode 100644 tests/workspaces/multi_template/.github/workflow-templates/package.extra.template.yml create mode 100644 tests/workspaces/multi_template/.github/workflow-templates/package.template.yml create mode 100644 tests/workspaces/multi_template/packages/pkg1/pyproject.toml create mode 100644 tests/workspaces/multi_template/pyproject.toml diff --git a/CHANGELOG.md b/CHANGELOG.md index 43cd324..f35a373 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,13 @@ All notable changes to the uv-workspace-codegen package will be documented in th The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [0.6.0] - 2025-11-25 + +### Added +- Support for multiple templates per `template_type`. +- Automatically discovers templates matching `..template.yml`. +- Generates workflows with name `--.yml`. + ## [0.5.0] - 2025-11-20 ### Added diff --git a/README.md b/README.md index f14bcfa..cfdf1f8 100644 --- a/README.md +++ b/README.md @@ -86,6 +86,11 @@ Templates are Jinja2 files that receive a `package` object with fields such as templates in the directory configured by `template_dir`. Create a file named `.template.yml` to support `template_type = ""`. +You can also create multiple templates for the same type by adding a suffix: +`..template.yml`. For example, `package.extra.template.yml` will +also be used for `template_type = "package"`, generating a workflow named +`package-extra-.yml`. + Template capabilities (examples): - inject package metadata diff --git a/pyproject.toml b/pyproject.toml index 1233a81..74e555a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "uv-workspace-codegen" -version = "0.5.0" +version = "0.6.0" description = "Generate individual CI/CD workflows for packages in the workspace" requires-python = ">=3.12" dependencies = [ diff --git a/src/uv_workspace_codegen/main.py b/src/uv_workspace_codegen/main.py index 0126b31..1e23679 100644 --- a/src/uv_workspace_codegen/main.py +++ b/src/uv_workspace_codegen/main.py @@ -166,65 +166,92 @@ def get_workspace_config(workspace_dir: Path) -> dict: return {} -def load_template( +def load_templates( template_type: str, workspace_dir: Path, workspace_config: dict, diff_mode: bool = False, -) -> Template: - """Load the appropriate template based on template type.""" +) -> list[tuple[str, Template]]: + """Load all templates matching the template type.""" # Get template directory from workspace config, with default fallback template_dir_str = workspace_config.get( "template_dir", ".github/workflow-templates" ) templates_dir = workspace_dir / template_dir_str - template_path = templates_dir / f"{template_type}.template.yml" - - # If the requested template does not exist, and the requested type is - # 'package', attempt to populate it from the bundled template located in - # this package's `templates/` directory. Only create the workspace - # templates directory when we actually need to write the default file. - if not template_path.exists(): - if template_type == "package": - bundled_template = ( - Path(__file__).parent / "templates" / "package.template.yml" - ) - if bundled_template.exists(): - try: - # In diff mode, we don't want to create the template file - if diff_mode: - with open(bundled_template, "r") as src: - return create_jinja_environment().from_string(src.read()) - - # Create templates dir now that we will populate it - templates_dir.mkdir(parents=True, exist_ok=True) - with ( - open(bundled_template, "r") as src, - open(template_path, "w") as dst, - ): - dst.write(src.read()) - except Exception: - # On any failure, raise a clear FileNotFoundError to match - # previous behavior for missing templates. - raise FileNotFoundError( - f"Template not found or could not be created: {template_path}" - ) - else: + + # Find all matching templates + # Pattern 1: {template_type}.template.yml (main template, empty suffix) + # Pattern 2: {template_type}.{suffix}.template.yml (extra templates) + + found_templates: list[tuple[str, Path]] = [] + + # Check for main template + main_template_path = templates_dir / f"{template_type}.template.yml" + if main_template_path.exists(): + found_templates.append(("", main_template_path)) + + # Check for extra templates + if templates_dir.exists(): + for path in templates_dir.glob(f"{template_type}.*.template.yml"): + # Extract suffix: template_type.suffix.template.yml + # name is template_type.suffix.template.yml + parts = path.name.split(".") + # parts should be [template_type, suffix, 'template', 'yml'] + if len(parts) == 4 and parts[0] == template_type and parts[2] == "template" and parts[3] == "yml": + suffix = parts[1] + found_templates.append((suffix, path)) + + # If no templates found, and type is 'package', try bundled template + if not found_templates and template_type == "package": + bundled_template = ( + Path(__file__).parent / "templates" / "package.template.yml" + ) + if bundled_template.exists(): + try: + # In diff mode, we don't want to create the template file + if diff_mode: + with open(bundled_template, "r") as src: + env = create_jinja_environment() + return [("", env.from_string(src.read()))] + + # Create templates dir now that we will populate it + templates_dir.mkdir(parents=True, exist_ok=True) + with ( + open(bundled_template, "r") as src, + open(main_template_path, "w") as dst, + ): + dst.write(src.read()) + found_templates.append(("", main_template_path)) + except Exception: + # On any failure, raise a clear FileNotFoundError to match + # previous behavior for missing templates. raise FileNotFoundError( - f"Bundled default template missing: {bundled_template}" + f"Template not found or could not be created: {main_template_path}" ) else: - raise FileNotFoundError(f"Template not found: {template_path}") - - with open(template_path, "r") as f: - template_content = f.read() + raise FileNotFoundError( + f"Bundled default template missing: {bundled_template}" + ) + elif not found_templates: + raise FileNotFoundError(f"No templates found for type: {template_type}") + results = [] env = create_jinja_environment() - return env.from_string(template_content) + + for suffix, path in found_templates: + with open(path, "r") as f: + template_content = f.read() + results.append((suffix, env.from_string(template_content))) + + return results def generate_workflow( - package: Package, template: Template, output_dir: Path, diff_mode: bool = False + package: Package, + template: Template, + output_dir: Path, + diff_mode: bool = False, + suffix: str = "" ) -> Optional[Path]: """Generate a workflow file for a single package.""" @@ -240,7 +267,10 @@ def generate_workflow( workflow_content = autogen_comment + workflow_content # Create workflow filename based on package name and template type - workflow_filename = f"{package.template_type}-{package.name}.yml" + if suffix: + workflow_filename = f"{package.template_type}-{suffix}-{package.name}.yml" + else: + workflow_filename = f"{package.template_type}-{package.name}.yml" workflow_path = output_dir / workflow_filename if diff_mode: @@ -361,23 +391,25 @@ def main(root_dir: Optional[Path], diff: bool): for package in packages: try: - # Load template if not cached + # Load templates if not cached if package.template_type not in templates_cache: - templates_cache[package.template_type] = load_template( + templates_cache[package.template_type] = load_templates( package.template_type, workspace_dir, workspace_config, diff_mode=diff, ) - template = templates_cache[package.template_type] - generated_file = generate_workflow( - package, template, workflows_dir, diff_mode=diff - ) - if ( - generated_file - ): # Only append if a file was actually generated (not in diff mode) - generated_files.append(generated_file) + templates = templates_cache[package.template_type] + + for suffix, template in templates: + generated_file = generate_workflow( + package, template, workflows_dir, diff_mode=diff, suffix=suffix + ) + if ( + generated_file + ): # Only append if a file was actually generated (not in diff mode) + generated_files.append(generated_file) except Exception as e: print(f"Error generating workflow for {package.name}: {e}") diff --git a/tests/test_main.py b/tests/test_main.py index 88a1f2a..cca0ab0 100644 --- a/tests/test_main.py +++ b/tests/test_main.py @@ -8,7 +8,7 @@ Package, discover_packages, get_workspace_config, - load_template, + load_templates, ) @@ -222,7 +222,7 @@ def test_get_workspace_config(): assert config == {"template_dir": "custom-templates"} -def test_load_template_configurable_dir(): +def test_load_templates_configurable_dir(): """Test loading templates from configurable directory.""" # Create a temporary directory structure @@ -248,22 +248,21 @@ def test_load_template_configurable_dir(): # Test with custom template directory configuration workspace_config = {"template_dir": "my-custom-templates"} - template = load_template("lib", workspace_dir, workspace_config) + templates = load_templates("lib", workspace_dir, workspace_config) # Verify the template loads correctly + assert len(templates) == 1 + suffix, template = templates[0] + assert suffix == "" assert template is not None # Test default template directory (should fail since we don't have templates there) workspace_config_default = {} try: - load_template("lib", workspace_dir, workspace_config_default) + load_templates("lib", workspace_dir, workspace_config_default) assert False, "Should have raised FileNotFoundError" except FileNotFoundError as e: - assert "Template not found" in str(e) - expected_path = os.path.join( - ".github", "workflow-templates", "lib.template.yml" - ) - assert expected_path in str(e) + assert "No templates found for type: lib" in str(e) def test_default_template_type(): diff --git a/tests/test_multi_template.py b/tests/test_multi_template.py new file mode 100644 index 0000000..ec9a03e --- /dev/null +++ b/tests/test_multi_template.py @@ -0,0 +1,32 @@ +from pathlib import Path + +from click.testing import CliRunner + +from uv_workspace_codegen.main import main + + +def test_multi_template_generation(): + workspace_dir = Path(__file__).parent / "workspaces" / "multi_template" + runner = CliRunner() + + # Run codegen + result = runner.invoke(main, [str(workspace_dir)]) + assert result.exit_code == 0 + + workflows_dir = workspace_dir / ".github" / "workflows" + + # Check main workflow + main_workflow = workflows_dir / "package-pkg1.yml" + assert main_workflow.exists() + content = main_workflow.read_text() + assert "Main template" in content + + # Check extra workflow + extra_workflow = workflows_dir / "package-extra-pkg1.yml" + assert extra_workflow.exists() + content = extra_workflow.read_text() + assert "Extra template" in content + + # Cleanup (optional, but good for local dev) + main_workflow.unlink() + extra_workflow.unlink() diff --git a/tests/workspaces/multi_template/.github/workflow-templates/package.extra.template.yml b/tests/workspaces/multi_template/.github/workflow-templates/package.extra.template.yml new file mode 100644 index 0000000..240c6e5 --- /dev/null +++ b/tests/workspaces/multi_template/.github/workflow-templates/package.extra.template.yml @@ -0,0 +1,7 @@ +name: {{ package.name }} Extra +on: push +jobs: + extra: + runs-on: ubuntu-latest + steps: + - run: echo "Extra template" diff --git a/tests/workspaces/multi_template/.github/workflow-templates/package.template.yml b/tests/workspaces/multi_template/.github/workflow-templates/package.template.yml new file mode 100644 index 0000000..5052452 --- /dev/null +++ b/tests/workspaces/multi_template/.github/workflow-templates/package.template.yml @@ -0,0 +1,7 @@ +name: {{ package.name }} +on: push +jobs: + build: + runs-on: ubuntu-latest + steps: + - run: echo "Main template" diff --git a/tests/workspaces/multi_template/packages/pkg1/pyproject.toml b/tests/workspaces/multi_template/packages/pkg1/pyproject.toml new file mode 100644 index 0000000..6a39d5e --- /dev/null +++ b/tests/workspaces/multi_template/packages/pkg1/pyproject.toml @@ -0,0 +1,5 @@ +[project] +name = "pkg1" + +[tool.uv-workspace-codegen] +generate = true diff --git a/tests/workspaces/multi_template/pyproject.toml b/tests/workspaces/multi_template/pyproject.toml new file mode 100644 index 0000000..cd92103 --- /dev/null +++ b/tests/workspaces/multi_template/pyproject.toml @@ -0,0 +1,5 @@ +[tool.uv.workspace] +members = ["packages/*"] + +[tool.uv-workspace-codegen] +default_template_type = "package" From 4a9ef1889cfa4626464c23ef7ffd1a3a8bcf041f Mon Sep 17 00:00:00 2001 From: Andrey Tatarinov Date: Wed, 26 Nov 2025 00:16:40 +0400 Subject: [PATCH 2/3] fix for gh actions at root --- .github/workflow-templates/package.template.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflow-templates/package.template.yml b/.github/workflow-templates/package.template.yml index 9397668..75dff8c 100644 --- a/.github/workflow-templates/package.template.yml +++ b/.github/workflow-templates/package.template.yml @@ -3,13 +3,13 @@ name: Test tool {{ package.name }} on: pull_request: paths: - - "{{ package.path }}/**" + - "{% if package.path != "." %}{{ package.path }}/{% endif %}**" # - "pyproject.toml" # - "uv.lock" push: branches: [master] paths: - - "{{ package.path }}/**" + - "{% if package.path != "." %}{{ package.path }}/{% endif %}**" - "pyproject.toml" - "uv.lock" From 212df001aa3c2f96de946924cf5f3ee52d18d1e5 Mon Sep 17 00:00:00 2001 From: Andrey Tatarinov Date: Wed, 26 Nov 2025 00:16:44 +0400 Subject: [PATCH 3/3] * --- uv.lock | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/uv.lock b/uv.lock index 6650cd0..73f3e4f 100644 --- a/uv.lock +++ b/uv.lock @@ -305,7 +305,7 @@ wheels = [ [[package]] name = "uv-workspace-codegen" -version = "0.5.0" +version = "0.6.0" source = { editable = "." } dependencies = [ { name = "click" },