From c217d59d929ef9f137a7df5f1e1eeff5e4b11415 Mon Sep 17 00:00:00 2001 From: Jon Zeolla Date: Fri, 19 Sep 2025 08:19:25 -0400 Subject: [PATCH 1/2] chore(ci): update actiosn --- .../.github/workflows/ci.yml" | 8 ++++---- .../.github/workflows/commit.yml" | 4 ++-- .../.github/workflows/release.yml" | 4 ++-- .../.github/workflows/security.yml" | 2 +- .../.github/workflows/update.yml" | 2 +- .../.github/workflows/validate_pr_titles.yml" | 2 +- 6 files changed, 11 insertions(+), 11 deletions(-) diff --git "a/{{cookiecutter.project_name|replace(\" \", \"\")}}/.github/workflows/ci.yml" "b/{{cookiecutter.project_name|replace(\" \", \"\")}}/.github/workflows/ci.yml" index f16970f..78ba8c1 100644 --- "a/{{cookiecutter.project_name|replace(\" \", \"\")}}/.github/workflows/ci.yml" +++ "b/{{cookiecutter.project_name|replace(\" \", \"\")}}/.github/workflows/ci.yml" @@ -23,7 +23,7 @@ jobs: contents: write steps: - name: Checkout the repository - uses: actions/checkout@v4 + uses: actions/checkout@v5 with: persist-credentials: 'false' - name: Bootstrap repository @@ -50,7 +50,7 @@ jobs: runs-on: ubuntu-24.04 steps: - name: Checkout the repository - uses: actions/checkout@v4 + uses: actions/checkout@v5 with: persist-credentials: 'false' - name: Bootstrap repository @@ -72,7 +72,7 @@ jobs: - linux/arm64 steps: - name: Checkout code - uses: actions/checkout@v4 + uses: actions/checkout@v5 with: persist-credentials: 'false' - name: Bootstrap repository @@ -147,7 +147,7 @@ jobs: exit 1 fi - name: Checkout the repository - uses: actions/checkout@v4 + uses: actions/checkout@v5 - name: Scan workflow logs for warnings and errors run: scripts/scan_workflow_logs.sh {% raw %}${{ github.run_id }}{% endraw %} env: diff --git "a/{{cookiecutter.project_name|replace(\" \", \"\")}}/.github/workflows/commit.yml" "b/{{cookiecutter.project_name|replace(\" \", \"\")}}/.github/workflows/commit.yml" index 2a8af3e..8b099cb 100644 --- "a/{{cookiecutter.project_name|replace(\" \", \"\")}}/.github/workflows/commit.yml" +++ "b/{{cookiecutter.project_name|replace(\" \", \"\")}}/.github/workflows/commit.yml" @@ -19,7 +19,7 @@ jobs: runs-on: ubuntu-24.04 steps: - name: Checkout the repository - uses: actions/checkout@v4 + uses: actions/checkout@v5 with: persist-credentials: 'false' - name: Bootstrap repository @@ -40,7 +40,7 @@ jobs: - linux/arm64 steps: - name: Checkout the repository - uses: actions/checkout@v4 + uses: actions/checkout@v5 with: fetch-depth: 0 persist-credentials: 'false' diff --git "a/{{cookiecutter.project_name|replace(\" \", \"\")}}/.github/workflows/release.yml" "b/{{cookiecutter.project_name|replace(\" \", \"\")}}/.github/workflows/release.yml" index 1e89f52..23ac38b 100644 --- "a/{{cookiecutter.project_name|replace(\" \", \"\")}}/.github/workflows/release.yml" +++ "b/{{cookiecutter.project_name|replace(\" \", \"\")}}/.github/workflows/release.yml" @@ -21,7 +21,7 @@ jobs: tag: ${{ "{{ steps.release.outputs.tag }}" }} steps: - name: Checkout the repository - uses: actions/checkout@v4 + uses: actions/checkout@v5 with: fetch-depth: 0 persist-credentials: 'false' @@ -56,7 +56,7 @@ jobs: runs-on: ubuntu-24.04 steps: - name: Checkout the repository - uses: actions/checkout@v4 + uses: actions/checkout@v5 with: fetch-depth: 0 ref: ${{ "{{ needs.release.outputs.tag }}" }} diff --git "a/{{cookiecutter.project_name|replace(\" \", \"\")}}/.github/workflows/security.yml" "b/{{cookiecutter.project_name|replace(\" \", \"\")}}/.github/workflows/security.yml" index 4949d77..9ca8362 100644 --- "a/{{cookiecutter.project_name|replace(\" \", \"\")}}/.github/workflows/security.yml" +++ "b/{{cookiecutter.project_name|replace(\" \", \"\")}}/.github/workflows/security.yml" @@ -31,7 +31,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout the repository - uses: actions/checkout@v4 + uses: actions/checkout@v5 with: persist-credentials: 'false' - name: Bootstrap repository diff --git "a/{{cookiecutter.project_name|replace(\" \", \"\")}}/.github/workflows/update.yml" "b/{{cookiecutter.project_name|replace(\" \", \"\")}}/.github/workflows/update.yml" index 3c726d1..72a3aee 100644 --- "a/{{cookiecutter.project_name|replace(\" \", \"\")}}/.github/workflows/update.yml" +++ "b/{{cookiecutter.project_name|replace(\" \", \"\")}}/.github/workflows/update.yml" @@ -22,7 +22,7 @@ jobs: pull-requests: write steps: - name: Checkout the repository - uses: actions/checkout@v4 + uses: actions/checkout@v5 with: persist-credentials: 'false' - name: Bootstrap repository diff --git "a/{{cookiecutter.project_name|replace(\" \", \"\")}}/.github/workflows/validate_pr_titles.yml" "b/{{cookiecutter.project_name|replace(\" \", \"\")}}/.github/workflows/validate_pr_titles.yml" index 4487979..5fefd5f 100644 --- "a/{{cookiecutter.project_name|replace(\" \", \"\")}}/.github/workflows/validate_pr_titles.yml" +++ "b/{{cookiecutter.project_name|replace(\" \", \"\")}}/.github/workflows/validate_pr_titles.yml" @@ -20,7 +20,7 @@ jobs: runs-on: ubuntu-latest steps: # https://github.com/amannn/action-semantic-pull-request/releases - - uses: amannn/action-semantic-pull-request@v5 + - uses: amannn/action-semantic-pull-request@v6 env: GITHUB_TOKEN: ${{ "{{ secrets.GITHUB_TOKEN }}" }} with: From 679e0e45158d1c4f03f57d0b2f79e6c789a168d5 Mon Sep 17 00:00:00 2001 From: Jon Zeolla Date: Tue, 23 Sep 2025 08:29:01 -0700 Subject: [PATCH 2/2] feat(naming): prevent invalid project names --- hooks/pre_gen_project.py | 39 +++++++++++++++++++++++++++++++ tests/test_cookiecutter.py | 47 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 86 insertions(+) create mode 100755 hooks/pre_gen_project.py diff --git a/hooks/pre_gen_project.py b/hooks/pre_gen_project.py new file mode 100755 index 0000000..61d21bc --- /dev/null +++ b/hooks/pre_gen_project.py @@ -0,0 +1,39 @@ +#!/usr/bin/env python3 +""" +Pre-project generation hook for validation +""" + +import re +import sys +from logging import basicConfig, getLogger + +basicConfig(level="WARNING", format="%(levelname)s: %(message)s") +LOG = getLogger("pre_generation_hook") + +# Cookiecutter variables +PROJECT_NAME = "{{ cookiecutter.project_name }}" +PROJECT_SLUG = "{{ cookiecutter.project_slug }}" + + +def validate_project_name() -> None: + """Validate that project_name starts with an alphabetical character.""" + # Check if project_name starts with an alphabetical character + if not re.match(r"^[a-zA-Z]", PROJECT_NAME): + LOG.error( + "Invalid project name '%s': Python project names must start with an alphabetical character (a-z or A-Z).", + PROJECT_NAME, + ) + sys.exit(1) + + +def validate_project_slug() -> None: + """Validate that project_slug is a valid Python identifier.""" + # Check if project_slug is a valid Python identifier + if not PROJECT_SLUG.isidentifier(): + LOG.error("Invalid project slug '%s': Must be a valid Python identifier.", PROJECT_SLUG) + sys.exit(1) + + +if __name__ == "__main__": + validate_project_name() + validate_project_slug() diff --git a/tests/test_cookiecutter.py b/tests/test_cookiecutter.py index 59c53aa..fdf9a03 100755 --- a/tests/test_cookiecutter.py +++ b/tests/test_cookiecutter.py @@ -211,6 +211,53 @@ def test_autofix_hook(cookies, context): pytest.fail(f"stdout: {error.stdout.decode('utf-8')}, stderr: {error.stderr.decode('utf-8')}") +@pytest.mark.unit +@pytest.mark.parametrize( + "invalid_name", + [ + "123invalid", # starts with number + "_invalid", # starts with underscore + "-invalid", # starts with dash + "9project", # starts with number + "!invalid", # starts with special character + ], +) +def test_invalid_project_name_validation(cookies, invalid_name): + """ + Test that project names starting with non-alphabetical characters are rejected + """ + result = cookies.bake(extra_context={"project_name": invalid_name}) + + # The generation should fail due to validation + assert result.exit_code != 0, f"Expected validation failure for project name: {invalid_name}" + + +@pytest.mark.unit +@pytest.mark.parametrize( + "valid_name", + [ + "ValidProject", # starts with uppercase + "validproject", # starts with lowercase + "My Project", # starts with uppercase, has space + "a1234", # starts with lowercase, has numbers + "Z_project", # starts with uppercase, has underscore + ], +) +def test_valid_project_name_validation(cookies, valid_name): + """ + Test that valid project names starting with alphabetical characters are accepted + """ + # Turn off the post generation hooks for faster testing + os.environ["RUN_POST_HOOK"] = "false" + + result = cookies.bake(extra_context={"project_name": valid_name}) + + # The generation should succeed + assert result.exit_code == 0, f"Expected validation success for project name: {valid_name}" + assert result.exception is None + assert result.project_path.is_dir() + + @pytest.mark.integration @pytest.mark.slow def test_default_project(cookies):