From 24f5dd188bee8ffa11ad5337fb8e1f2c80863015 Mon Sep 17 00:00:00 2001 From: dronefreak Date: Wed, 4 Feb 2026 16:50:46 +0100 Subject: [PATCH 1/6] feat: Fix files as per pre commits Signed-off-by: dronefreak --- .github/PULL_REQUEST_TEMPLATE.md | 20 ++++ .github/dependabot.yml | 28 +++++ .github/workflows/auto-populate-pr.yml | 111 ++++++++++++++++++ .github/workflows/ci.yml | 81 +++++++++++++ .github/workflows/dependency-review.yml | 24 ++++ .github/workflows/pr-body.yml | 54 +++++++++ .github/workflows/pr-policy.yml | 63 ++++++++++ .pre-commit-config.yaml | 15 +-- .yamllint | 34 ++++++ apps/decrement_hydra.py | 18 ++- apps/increment_fire.py | 6 +- docker/Dockerfile | 2 +- docker/compose.yaml | 8 +- docker/create.sh | 2 +- docker/exec.sh | 2 +- pyproject.toml | 35 ++++-- requirements.txt | 6 + setup.py | 4 +- src/PROJECT_NAME/__init__.py | 0 src/template_project/__init__.py | 1 + src/{PROJECT_NAME => template_project}/add.py | 5 +- tests/__init__.py | 1 + tests/apps/__init__.py | 1 + tests/apps/test_decrement_hydra.py | 5 +- tests/apps/test_increment_fire.py | 5 +- tests/test_add.py | 7 +- 26 files changed, 488 insertions(+), 50 deletions(-) create mode 100644 .github/PULL_REQUEST_TEMPLATE.md create mode 100644 .github/dependabot.yml create mode 100644 .github/workflows/auto-populate-pr.yml create mode 100644 .github/workflows/ci.yml create mode 100644 .github/workflows/dependency-review.yml create mode 100644 .github/workflows/pr-body.yml create mode 100644 .github/workflows/pr-policy.yml create mode 100644 .yamllint delete mode 100644 src/PROJECT_NAME/__init__.py create mode 100644 src/template_project/__init__.py rename src/{PROJECT_NAME => template_project}/add.py (87%) create mode 100644 tests/apps/__init__.py diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md new file mode 100644 index 0000000..0bc53ef --- /dev/null +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -0,0 +1,20 @@ +## Summary + + + +## Changes + + + +- + +## Testing + + + +- [ ] Tests pass locally (`python -m pytest tests/ -v`) +- [ ] Pre-commit hooks pass (`pre-commit run --all-files`) + +## Related Issues + + diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..269f7fc --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,28 @@ +version: 2 + +updates: + # Python (pip) dependencies + - package-ecosystem: "pip" + directory: "/" + schedule: + interval: "weekly" + day: "monday" + open-pull-requests-limit: 10 + reviewers: [] + labels: + - "dependencies" + commit-message: + prefix: "chore(deps)" + + # GitHub Actions + - package-ecosystem: "github-actions" + directory: "/" + schedule: + interval: "weekly" + day: "monday" + open-pull-requests-limit: 5 + labels: + - "dependencies" + - "ci" + commit-message: + prefix: "ci(deps)" diff --git a/.github/workflows/auto-populate-pr.yml b/.github/workflows/auto-populate-pr.yml new file mode 100644 index 0000000..9e1b56f --- /dev/null +++ b/.github/workflows/auto-populate-pr.yml @@ -0,0 +1,111 @@ +name: Auto-populate PR Body + +on: + pull_request: + types: [opened] + +permissions: + pull-requests: write + +jobs: + populate-body: + name: Generate PR Body + runs-on: ubuntu-22.04 + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Generate and update PR body + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + PR_NUMBER: ${{ github.event.pull_request.number }} + BASE_SHA: ${{ github.event.pull_request.base.sha }} + HEAD_SHA: ${{ github.event.pull_request.head.sha }} + run: | + # Fetch the current PR body + CURRENT_BODY=$(gh pr view "$PR_NUMBER" --json body -q '.body') + + # Strip HTML comments and whitespace to check for real content + STRIPPED=$(echo "$CURRENT_BODY" | sed 's///g; s/^[[:space:]]*//; s/[[:space:]]*$//; /^$/d; s/^- *$//') + STRIPPED=$(echo "$STRIPPED" | sed '/^## /d') + STRIPPED=$(echo "$STRIPPED" | sed 's/^[[:space:]]*//; s/[[:space:]]*$//; /^$/d') + + # Check if it contains only the unfilled template or is empty + HAS_CHECKLIST=$(echo "$STRIPPED" | grep -c '\[.\]' || true) + NON_CHECKLIST=$(echo "$STRIPPED" | grep -cv '\[.\]' || true) + + if [[ "$NON_CHECKLIST" -gt 0 ]]; then + echo "PR body already has custom content. Skipping auto-populate." + exit 0 + fi + + echo "PR body is empty or template-only. Generating content..." + + # --- Collect commit messages --- + COMMITS=$(git log --pretty=format:"- %s" "$BASE_SHA".."$HEAD_SHA" 2>/dev/null || \ + git log --pretty=format:"- %s" -20) + + # --- Collect changed files with stats --- + DIFF_STAT=$(git diff --stat "$BASE_SHA".."$HEAD_SHA" 2>/dev/null || \ + git diff --stat HEAD~1) + + FILE_LIST=$(git diff --name-only "$BASE_SHA".."$HEAD_SHA" 2>/dev/null || \ + git diff --name-only HEAD~1) + + # --- Categorize changes --- + ADDED=$(echo "$FILE_LIST" | grep -c '.' || true) + SRC_CHANGES=$(echo "$FILE_LIST" | grep -c '^src/' || true) + TEST_CHANGES=$(echo "$FILE_LIST" | grep -c '^tests/' || true) + DOC_CHANGES=$(echo "$FILE_LIST" | grep -cE '^(.github/.*\.md|.*README)' || true) + CI_CHANGES=$(echo "$FILE_LIST" | grep -cE '^\.github/(workflows|dependabot)' || true) + + # --- Build summary line --- + SUMMARY_PARTS=() + [[ "$SRC_CHANGES" -gt 0 ]] && SUMMARY_PARTS+=("$SRC_CHANGES source file(s)") + [[ "$TEST_CHANGES" -gt 0 ]] && SUMMARY_PARTS+=("$TEST_CHANGES test file(s)") + [[ "$DOC_CHANGES" -gt 0 ]] && SUMMARY_PARTS+=("$DOC_CHANGES doc file(s)") + [[ "$CI_CHANGES" -gt 0 ]] && SUMMARY_PARTS+=("$CI_CHANGES CI file(s)") + + if [[ ${#SUMMARY_PARTS[@]} -gt 0 ]]; then + SUMMARY_LINE="This PR touches $(IFS=', '; echo "${SUMMARY_PARTS[*]}") across $ADDED file(s) total." + else + SUMMARY_LINE="This PR modifies $ADDED file(s)." + fi + + # --- Build the PR body --- + BODY=$(cat < + Diff stats + + \`\`\` + $DIFF_STAT + \`\`\` + + + + ## Testing + + - [ ] Tests pass locally (\`python -m pytest tests/ -v\`) + - [ ] Pre-commit hooks pass (\`pre-commit run --all-files\`) + + ## Related Issues + + + EOF + ) + + # Remove leading whitespace from heredoc indentation + BODY=$(echo "$BODY" | sed 's/^ //') + + # Update the PR body + gh pr edit "$PR_NUMBER" --body "$BODY" + echo "PR body has been auto-populated." diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..581811a --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,81 @@ +name: CI + +on: + push: + branches: [main] + pull_request: + branches: [main] + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +jobs: + lint: + name: Lint & Format + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - uses: actions/setup-python@v5 + with: + python-version: "3.11" + + - name: Install dependencies + run: | + pip install --upgrade pip + pip install pre-commit mypy bandit ruff + + - name: Cache pre-commit hooks + uses: actions/cache@v4 + with: + path: ~/.cache/pre-commit + key: pre-commit-${{ hashFiles('.pre-commit-config.yaml') }} + restore-keys: pre-commit- + + - name: Run pre-commit + run: pre-commit run --all-files --show-diff-on-failure + + - name: Lint check with ruff + run: ruff check src/ tests/ apps/ + + - name: Type check with mypy + run: mypy src/ apps/ + + - name: Security check with bandit + run: bandit -c pyproject.toml -r src/ apps/ + + test: + name: Test (Python ${{ matrix.python-version }}, ${{ matrix.os }}) + runs-on: ${{ matrix.os }} + needs: lint + strategy: + fail-fast: false + matrix: + os: [ubuntu-latest, macos-latest, windows-latest] + python-version: ["3.9", "3.10", "3.11", "3.12"] + steps: + - uses: actions/checkout@v4 + + - uses: actions/setup-python@v5 + with: + python-version: ${{ matrix.python-version }} + + - name: Install dependencies + run: | + pip install --upgrade pip + pip install -r requirements.txt + pip install -e . + + - name: Run tests + run: python -m pytest tests/ -v + + - name: Run tests with coverage + run: | + python -m pytest tests/ \ + -v \ + --cov=template_project \ + --cov=apps \ + --cov-fail-under=90 \ + --cov-report=term-missing \ + --cov-report=xml diff --git a/.github/workflows/dependency-review.yml b/.github/workflows/dependency-review.yml new file mode 100644 index 0000000..c8b5542 --- /dev/null +++ b/.github/workflows/dependency-review.yml @@ -0,0 +1,24 @@ +name: Dependency Review + +on: + pull_request: + paths: + - "requirements*.txt" + - "setup.py" + - "pyproject.toml" + +permissions: + contents: read + +jobs: + dependency-review: + name: Review Dependencies + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Dependency Review + uses: actions/dependency-review-action@v4 + with: + fail-on-severity: high + comment-summary-in-pr: always diff --git a/.github/workflows/pr-body.yml b/.github/workflows/pr-body.yml new file mode 100644 index 0000000..17c391e --- /dev/null +++ b/.github/workflows/pr-body.yml @@ -0,0 +1,54 @@ +name: PR Body Validation + +on: + pull_request: + # Runs on 'edited' and 'reopened' only — the 'opened' event is handled by + # the auto-populate workflow, which edits the body and triggers 'edited'. + types: [edited, reopened] + +jobs: + validate-body: + name: Validate PR Body + runs-on: ubuntu-22.04 + steps: + - name: Check PR body is not empty + run: | + BODY=$(cat <<'BODY_EOF' + ${{ github.event.pull_request.body }} + BODY_EOF + ) + + # Strip HTML comments, whitespace, and section headers + CLEANED=$(echo "$BODY" | sed 's///g; /^## /d; /^- \[ \]/d; s/^[[:space:]]*//; s/[[:space:]]*$//; /^$/d; /^-$/d') + + if [[ -z "$CLEANED" ]]; then + echo "::error::PR body is empty. Please provide a description of your changes." + echo "" + echo "A good PR description should include:" + echo " - What: A summary of the changes made" + echo " - Why: The motivation or issue being addressed" + echo " - How: Key implementation details (if non-obvious)" + echo " - Testing: How the changes were verified" + exit 1 + fi + + # Check minimum length (at least 20 characters of meaningful content) + CHAR_COUNT=${#CLEANED} + if [[ "$CHAR_COUNT" -lt 20 ]]; then + echo "::error::PR body is too short ($CHAR_COUNT chars). Please provide a meaningful description." + exit 1 + fi + + echo "PR body is present ($CHAR_COUNT chars)." + + - name: Check for placeholder text + run: | + BODY=$(cat <<'BODY_EOF' + ${{ github.event.pull_request.body }} + BODY_EOF + ) + + # Warn if common placeholder patterns are detected + if echo "$BODY" | grep -qiE '(TODO|FIXME|PLACEHOLDER|fill in|describe here|add description)'; then + echo "::warning::PR body may contain placeholder text. Please ensure all sections are filled in." + fi diff --git a/.github/workflows/pr-policy.yml b/.github/workflows/pr-policy.yml new file mode 100644 index 0000000..5f20b81 --- /dev/null +++ b/.github/workflows/pr-policy.yml @@ -0,0 +1,63 @@ +name: PR Policy + +on: + pull_request: + types: [opened, edited, synchronize, labeled, unlabeled, reopened] + +jobs: + title-convention: + name: Validate PR Title + runs-on: ubuntu-latest + steps: + - name: Check conventional commit format + run: | + TITLE="${{ github.event.pull_request.title }}" + PATTERN="^(feat|fix|docs|doc|style|refactor|perf|test|build|ci|chore|revert)(\(.+\))?!?: .+" + if [[ ! "$TITLE" =~ $PATTERN ]]; then + echo "::error::PR title does not follow Conventional Commits format." + echo "" + echo "Expected format: (): " + echo " Types: feat, fix, docs, doc, style, refactor, perf, test, build, ci, chore, revert" + echo "" + echo "Examples:" + echo " feat: add decrement CLI command" + echo " fix(core): handle empty config file" + echo " docs: update installation instructions" + echo "" + echo "Got: '$TITLE'" + exit 1 + fi + echo "PR title follows Conventional Commits format." + + label-check: + name: Require Label + runs-on: ubuntu-latest + steps: + - name: Check for at least one label + run: | + LABEL_COUNT=$(echo '${{ toJson(github.event.pull_request.labels) }}' | jq 'length') + if [[ "$LABEL_COUNT" -eq 0 ]]; then + echo "::error::PR must have at least one label before merging." + echo "Consider adding a label such as: bug, enhancement, documentation, maintenance, etc." + exit 1 + fi + echo "PR has $LABEL_COUNT label(s)." + + branch-naming: + name: Validate Branch Name + runs-on: ubuntu-latest + steps: + - name: Check branch naming convention + run: | + BRANCH="${{ github.head_ref }}" + PATTERN="^(feature|fix|bugfix|hotfix|docs|chore|refactor|test|ci|release|claude)/" + if [[ ! "$BRANCH" =~ $PATTERN ]]; then + echo "::warning::Branch name '$BRANCH' does not follow the recommended naming convention." + echo "" + echo "Recommended format: /" + echo " Types: feature, fix, bugfix, hotfix, docs, chore, refactor, test, ci, release" + echo "" + echo "Examples:" + echo " feature/add-decrement-cli" + echo " fix/empty-config-handling" + fi diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 8b071b9..4a0792c 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -4,7 +4,6 @@ # - Python linting + formatting (Ruff) # - Typing checks (Mypy) # - Security checks (Bandit) -# - Docstring auto-formatting (Docformatter) # - Docs build smoke test # - Commit message enforcement # - Shell script linting (shfmt + shellcheck) @@ -44,14 +43,6 @@ repos: - id: ruff-format types_or: [python, pyi] - # ---------- DOCSTRING AUTO-FIX (Docformatter) ---------- - - repo: https://github.com/PyCQA/docformatter - rev: v1.7.7 - hooks: - - id: docformatter - args: [--in-place, --recursive, --wrap-summaries=88] - types_or: [python] - # ---------- TYPES (MYPY) ---------- - repo: https://github.com/pre-commit/mirrors-mypy rev: v1.19.1 @@ -125,11 +116,7 @@ repos: rev: v1.38.0 hooks: - id: yamllint - args: - [ - -d, - "{extends: default, rules: {line-length: {max: 120}, document-start: disable}}", - ] + args: [--config-file=.yamllint] # ---------- CI / PRE-COMMIT.CI CONFIG ---------- ci: diff --git a/.yamllint b/.yamllint new file mode 100644 index 0000000..efa8b92 --- /dev/null +++ b/.yamllint @@ -0,0 +1,34 @@ +# .yamllint +extends: default + +rules: + # GitHub Actions REQUIRE unquoted true/false for certain keys + truthy: + allowed-values: ["true", "false", "on", "off", "yes", "no"] + check-keys: false # Critical: don't validate GitHub Actions boolean keys + + # Realistic line lengths for CI workflows (URLs, commands) + line-length: + max: 160 + allow-non-breakable-words: true + allow-non-breakable-inline-mappings: true + + # Enforce newline at EOF (pre-commit already does this) + new-lines: + type: unix + + # Ignore GitHub Actions schema-specific constructs + comments-indentation: disable + indentation: + indent-sequences: consistent + +# Apply stricter rules to project YAML (configs), relaxed for workflows +yaml-files: + - '*.yaml' + - '*.yml' + +ignore: | + .git/ + __pycache__/ + .venv/ + venv/ diff --git a/apps/decrement_hydra.py b/apps/decrement_hydra.py index 3178380..fe7130e 100644 --- a/apps/decrement_hydra.py +++ b/apps/decrement_hydra.py @@ -1,26 +1,36 @@ """Sample app decrementing a given number using Hydra to handle CLI.""" import hydra +from omegaconf import DictConfig -from PROJECT_NAME.add import add +from template_project.add import add def decrement(x: int) -> int: - """Return the decrement of a given value. + """ + Return the decrement of a given value. Args: x (int): value to be decremented Returns: int: The decremented value 'x-1' + """ return add(x, -1) @hydra.main(version_base=None, config_path="configs", config_name="decrement") -def main(config): +def main(config: DictConfig) -> None: + """ + Decrement the given value from config and print the result. + + Args: + config: Hydra config object with 'value' attribute + + """ result = decrement(config.value) - print(result) + print(result) # noqa: T201 if __name__ == "__main__": diff --git a/apps/increment_fire.py b/apps/increment_fire.py index 832e28d..9c52e16 100644 --- a/apps/increment_fire.py +++ b/apps/increment_fire.py @@ -2,17 +2,19 @@ import fire -from PROJECT_NAME.add import add +from template_project.add import add def increment(x: int) -> int: - """Return the increment of a given value. + """ + Return the increment of a given value. Args: x (int): value to be incremented Returns: int: The incremented value 'x+1' + """ return add(x, 1) diff --git a/docker/Dockerfile b/docker/Dockerfile index 89506ea..5af147d 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -15,4 +15,4 @@ RUN apt update && \ echo "$USERNAME ALL=(ALL) NOPASSWD: ALL" >> /etc/sudoers USER $USERNAME -WORKDIR /workspace/PROJECT_NAME +WORKDIR /workspace/template_project diff --git a/docker/compose.yaml b/docker/compose.yaml index 7042e1c..0450e94 100644 --- a/docker/compose.yaml +++ b/docker/compose.yaml @@ -1,7 +1,7 @@ services: - project_name: - image: project_name - container_name: project_name_container + template_project: + image: template_project + container_name: template_project_container build: context: . args: @@ -17,6 +17,6 @@ services: capabilities: [gpu] command: sleep infinity volumes: - - ..:/workspace/PROJECT_NAME + - ..:/workspace/template_project - ${HOST_SSH_FOLDER}:/home/${HOST_USERNAME}/.ssh:ro - ${HOST_GITCONFIG_FILE}:/home/${HOST_USERNAME}/.gitconfig:ro diff --git a/docker/create.sh b/docker/create.sh index 0a623b6..d8f50b8 100755 --- a/docker/create.sh +++ b/docker/create.sh @@ -12,4 +12,4 @@ source set_env_variables.sh docker compose up -d # Get an interactive bash session in the container -docker compose exec project_name bash +docker compose exec template_project bash diff --git a/docker/exec.sh b/docker/exec.sh index 2026e39..bda1519 100755 --- a/docker/exec.sh +++ b/docker/exec.sh @@ -9,4 +9,4 @@ cd "$(dirname "$0")" || exit source set_env_variables.sh # Get an interactive bash session -docker compose exec project_name bash +docker compose exec template_project bash diff --git a/pyproject.toml b/pyproject.toml index 8f6e425..572d89c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,21 +1,30 @@ [tool.ruff] -# Core linting + formatting line-length = 88 -target-version = ["py39", "py310", "py311", "py312"] -fix = true -select = ["ALL"] +target-version = "py39" +src = ["src", "apps", "tests"] + +[tool.ruff.lint] +select = ["E", "F", "W", "B", "S", "D", "PLR"] extend-ignore = [ - "E203", # Conflicts with line break before ':' in slices (Black style) - "W503" # Line break before binary operator (Black style) + "E203", # Black-compatible slice formatting + "D203", # Google style: no blank line before class (keep D211) + "D212", # Google style: multi-line summary on second line (keep D213) ] -src = ["src", "apps", "tests"] -[tool.docformatter] -# Auto-format docstrings -wrap-summaries = 88 -pre-summary-newline = true -make-summary-multi-line = true -recursive = true +[tool.ruff.lint.per-file-ignores] +"tests/**/*.py" = [ + "D100", "D101", "D102", # No docstring requirements in tests + "S101", # allow assert + "S602", # shell=True in subprocess (tests only) + "PLR2004", # Magic values in assertions are normal +] + +[tool.ruff.format] +quote-style = "double" +indent-style = "space" +skip-magic-trailing-comma = false +line-ending = "auto" + [tool.mypy] # Type checking diff --git a/requirements.txt b/requirements.txt index 4f9c3aa..2292cab 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,10 @@ +bandit fire hydra-core +mypy pre-commit<4 pytest +pytest-cov +pytest-mock +ruff +types-PyYAML diff --git a/setup.py b/setup.py index 6c701d8..49e5985 100644 --- a/setup.py +++ b/setup.py @@ -1,7 +1,9 @@ +"""Docstring for setup.""" + from setuptools import find_packages, setup setup( - name="PROJECT_NAME", + name="template_project", version="0.0.0", description="", author="", diff --git a/src/PROJECT_NAME/__init__.py b/src/PROJECT_NAME/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/src/template_project/__init__.py b/src/template_project/__init__.py new file mode 100644 index 0000000..c10b7ed --- /dev/null +++ b/src/template_project/__init__.py @@ -0,0 +1 @@ +"""Docstring for template_project.""" diff --git a/src/PROJECT_NAME/add.py b/src/template_project/add.py similarity index 87% rename from src/PROJECT_NAME/add.py rename to src/template_project/add.py index aa39836..6124e82 100644 --- a/src/PROJECT_NAME/add.py +++ b/src/template_project/add.py @@ -2,7 +2,8 @@ def add(a: int, b: int) -> int: - """Return the sum of two numbers. + """ + Return the sum of two numbers. Adds two numbers using internally the '+' operator. @@ -12,6 +13,6 @@ def add(a: int, b: int) -> int: Returns: int: The value 'a+b' - """ + """ return a + b diff --git a/tests/__init__.py b/tests/__init__.py index e69de29..40c103b 100644 --- a/tests/__init__.py +++ b/tests/__init__.py @@ -0,0 +1 @@ +"""Docstring for apps/__init__.py.""" diff --git a/tests/apps/__init__.py b/tests/apps/__init__.py new file mode 100644 index 0000000..56fced9 --- /dev/null +++ b/tests/apps/__init__.py @@ -0,0 +1 @@ +"""Docstring for tests.apps.""" diff --git a/tests/apps/test_decrement_hydra.py b/tests/apps/test_decrement_hydra.py index 93720bb..1ffac58 100644 --- a/tests/apps/test_decrement_hydra.py +++ b/tests/apps/test_decrement_hydra.py @@ -1,3 +1,5 @@ +"""Tests for the decrement function.""" + import subprocess import tempfile from pathlib import Path @@ -5,9 +7,8 @@ apps_directory_path = Path(__file__).parents[2] / "apps" -def test_increment_fire(): +def test_increment_fire() -> None: """Test the decrement_hydra.py script.""" - with tempfile.TemporaryDirectory() as tmp_dir: command = ( f"python {apps_directory_path / 'decrement_hydra.py'} " diff --git a/tests/apps/test_increment_fire.py b/tests/apps/test_increment_fire.py index 48081b1..6555aa2 100644 --- a/tests/apps/test_increment_fire.py +++ b/tests/apps/test_increment_fire.py @@ -1,12 +1,13 @@ +"""Tests for the increment function.""" + import subprocess from pathlib import Path apps_directory_path = Path(__file__).parents[2] / "apps" -def test_increment_fire(): +def test_increment_fire() -> None: """Test the increment_fire.py script.""" - command = f"python {apps_directory_path / 'increment_fire.py'} --x 2" res = int(subprocess.check_output(command, shell=True)) assert res == 3 diff --git a/tests/test_add.py b/tests/test_add.py index 6bb8101..93be505 100644 --- a/tests/test_add.py +++ b/tests/test_add.py @@ -1,7 +1,8 @@ -from PROJECT_NAME.add import add +"""Tests for the add function.""" +from template_project.add import add -def test_add(): - """Test of the add module.""" +def test_add() -> None: + """Test of the add module.""" assert add(0, 1) == 1 From f033cc513c2951bb528d5c897363641721dab656 Mon Sep 17 00:00:00 2001 From: dronefreak Date: Wed, 4 Feb 2026 16:56:25 +0100 Subject: [PATCH 2/6] fix: Removed reviewers: [] entirely - it's not a valid Dependabot key Signed-off-by: dronefreak --- .github/dependabot.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 269f7fc..056f1bb 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -8,7 +8,6 @@ updates: interval: "weekly" day: "monday" open-pull-requests-limit: 10 - reviewers: [] labels: - "dependencies" commit-message: From 3f81c3fd4d8ffcc3560a2f8c0cb19be31ac21377 Mon Sep 17 00:00:00 2001 From: dronefreak Date: Wed, 4 Feb 2026 17:07:17 +0100 Subject: [PATCH 3/6] fix: Fixed auto populate file Signed-off-by: dronefreak --- .github/workflows/auto-populate-pr.yml | 110 ++++++++++++------------- .github/workflows/ci.yml | 1 - 2 files changed, 54 insertions(+), 57 deletions(-) diff --git a/.github/workflows/auto-populate-pr.yml b/.github/workflows/auto-populate-pr.yml index 9e1b56f..b4c2996 100644 --- a/.github/workflows/auto-populate-pr.yml +++ b/.github/workflows/auto-populate-pr.yml @@ -16,45 +16,45 @@ jobs: with: fetch-depth: 0 + - name: Fetch base branch + run: git fetch origin ${{ github.event.pull_request.base.ref }} --depth=1 + - name: Generate and update PR body env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} PR_NUMBER: ${{ github.event.pull_request.number }} - BASE_SHA: ${{ github.event.pull_request.base.sha }} - HEAD_SHA: ${{ github.event.pull_request.head.sha }} + BASE_REF: ${{ github.event.pull_request.base.ref }} run: | # Fetch the current PR body CURRENT_BODY=$(gh pr view "$PR_NUMBER" --json body -q '.body') - # Strip HTML comments and whitespace to check for real content - STRIPPED=$(echo "$CURRENT_BODY" | sed 's///g; s/^[[:space:]]*//; s/[[:space:]]*$//; /^$/d; s/^- *$//') - STRIPPED=$(echo "$STRIPPED" | sed '/^## /d') - STRIPPED=$(echo "$STRIPPED" | sed 's/^[[:space:]]*//; s/[[:space:]]*$//; /^$/d') - - # Check if it contains only the unfilled template or is empty - HAS_CHECKLIST=$(echo "$STRIPPED" | grep -c '\[.\]' || true) - NON_CHECKLIST=$(echo "$STRIPPED" | grep -cv '\[.\]' || true) + # Strip HTML comments, headers, whitespace, checklist items + STRIPPED=$(echo "$CURRENT_BODY" \ + | sed 's///g' \ + | sed '/^## /d' \ + | sed '/^- \[.\]/d' \ + | sed '/^-$/d' \ + | sed 's/^[[:space:]]*//; s/[[:space:]]*$//' \ + | sed '/^$/d') - if [[ "$NON_CHECKLIST" -gt 0 ]]; then + if [[ -n "$STRIPPED" ]]; then echo "PR body already has custom content. Skipping auto-populate." exit 0 fi echo "PR body is empty or template-only. Generating content..." + MERGE_BASE=$(git merge-base origin/"$BASE_REF" HEAD) + # --- Collect commit messages --- - COMMITS=$(git log --pretty=format:"- %s" "$BASE_SHA".."$HEAD_SHA" 2>/dev/null || \ - git log --pretty=format:"- %s" -20) + COMMITS=$(git log --pretty=format:"- %s" "$MERGE_BASE"..HEAD) # --- Collect changed files with stats --- - DIFF_STAT=$(git diff --stat "$BASE_SHA".."$HEAD_SHA" 2>/dev/null || \ - git diff --stat HEAD~1) - - FILE_LIST=$(git diff --name-only "$BASE_SHA".."$HEAD_SHA" 2>/dev/null || \ - git diff --name-only HEAD~1) + DIFF_STAT=$(git diff --stat "$MERGE_BASE"..HEAD) + FILE_LIST=$(git diff --name-only "$MERGE_BASE"..HEAD) # --- Categorize changes --- - ADDED=$(echo "$FILE_LIST" | grep -c '.' || true) + TOTAL=$(echo "$FILE_LIST" | grep -c '.' || true) SRC_CHANGES=$(echo "$FILE_LIST" | grep -c '^src/' || true) TEST_CHANGES=$(echo "$FILE_LIST" | grep -c '^tests/' || true) DOC_CHANGES=$(echo "$FILE_LIST" | grep -cE '^(.github/.*\.md|.*README)' || true) @@ -68,44 +68,42 @@ jobs: [[ "$CI_CHANGES" -gt 0 ]] && SUMMARY_PARTS+=("$CI_CHANGES CI file(s)") if [[ ${#SUMMARY_PARTS[@]} -gt 0 ]]; then - SUMMARY_LINE="This PR touches $(IFS=', '; echo "${SUMMARY_PARTS[*]}") across $ADDED file(s) total." + SUMMARY_LINE="This PR touches $(IFS=', '; echo "${SUMMARY_PARTS[*]}") across $TOTAL file(s) total." else - SUMMARY_LINE="This PR modifies $ADDED file(s)." + SUMMARY_LINE="This PR modifies $TOTAL file(s)." fi - # --- Build the PR body --- - BODY=$(cat < - Diff stats - - \`\`\` - $DIFF_STAT - \`\`\` - - - - ## Testing - - - [ ] Tests pass locally (\`python -m pytest tests/ -v\`) - - [ ] Pre-commit hooks pass (\`pre-commit run --all-files\`) - - ## Related Issues - - - EOF - ) - - # Remove leading whitespace from heredoc indentation - BODY=$(echo "$BODY" | sed 's/^ //') - - # Update the PR body - gh pr edit "$PR_NUMBER" --body "$BODY" + # --- Write PR body to a temp file --- + BODY_FILE=$(mktemp) + { + echo "## Summary" + echo "" + echo "$SUMMARY_LINE" + echo "" + echo "## Changes" + echo "" + echo "$COMMITS" + echo "" + echo "
" + echo "Diff stats" + echo "" + echo '```' + echo "$DIFF_STAT" + echo '```' + echo "" + echo "
" + echo "" + echo "## Testing" + echo "" + echo '- [ ] Tests pass locally (`python -m pytest tests/ -v`)' + echo '- [ ] Pre-commit hooks pass (`pre-commit run --all-files`)' + echo "" + echo "## Related Issues" + echo "" + echo "" + } > "$BODY_FILE" + + # Update the PR body from file + gh pr edit "$PR_NUMBER" --body-file "$BODY_FILE" + rm -f "$BODY_FILE" echo "PR body has been auto-populated." diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 581811a..142b353 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -75,7 +75,6 @@ jobs: python -m pytest tests/ \ -v \ --cov=template_project \ - --cov=apps \ --cov-fail-under=90 \ --cov-report=term-missing \ --cov-report=xml From 7c6390b4fb0f5b51b441a7af9fbe414aeecebf10 Mon Sep 17 00:00:00 2001 From: dronefreak Date: Thu, 5 Feb 2026 14:33:54 +0100 Subject: [PATCH 4/6] chore: Removed windows-latest OS Signed-off-by: dronefreak --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 142b353..4c164fd 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -52,7 +52,7 @@ jobs: strategy: fail-fast: false matrix: - os: [ubuntu-latest, macos-latest, windows-latest] + os: [ubuntu-latest, macos-latest] python-version: ["3.9", "3.10", "3.11", "3.12"] steps: - uses: actions/checkout@v4 From c3b489e1fc84823e43f12a0e8fb4801fbdf43e56 Mon Sep 17 00:00:00 2001 From: dronefreak Date: Wed, 11 Feb 2026 17:21:42 +0100 Subject: [PATCH 5/6] feat: Update github workflows with timeouts; Add PR body script; Add requirements-dev.txt Signed-off-by: dronefreak --- .github/scripts/generate_pr_body.py | 130 ++++++++++++++++++++++++ .github/workflows/auto-populate-pr.yml | 90 +++++----------- .github/workflows/ci.yml | 34 ++++++- .github/workflows/dependency-review.yml | 1 + .github/workflows/pr-body.yml | 7 +- .github/workflows/pr-policy.yml | 6 ++ requirements-dev.txt | 7 ++ 7 files changed, 202 insertions(+), 73 deletions(-) create mode 100755 .github/scripts/generate_pr_body.py create mode 100644 requirements-dev.txt diff --git a/.github/scripts/generate_pr_body.py b/.github/scripts/generate_pr_body.py new file mode 100755 index 0000000..4db8604 --- /dev/null +++ b/.github/scripts/generate_pr_body.py @@ -0,0 +1,130 @@ +#!/usr/bin/env python3 +"""Generate PR body from git history.""" + +import os +import subprocess +import sys + + +def run_cmd(cmd: list[str]) -> str: + """ + Run command and return stdout. + + Note: Commands use validated inputs only. The BASE_REF is validated + in main() before being used in any git commands. + """ + result = subprocess.run( # nosec B603 # noqa: S603 + cmd, + capture_output=True, + text=True, + check=True, + shell=False, # Explicit: never use shell + ) + return result.stdout.strip() + + +def categorize_files(files: list[str]) -> dict[str, int]: + """Categorize changed files.""" + categories = { + "src": 0, + "test": 0, + "doc": 0, + "ci": 0, + } + + for f in files: + if f.startswith("src/"): + categories["src"] += 1 + elif f.startswith("tests/"): + categories["test"] += 1 + elif f.endswith("README.md") or (".github/" in f and f.endswith(".md")): + categories["doc"] += 1 + elif ".github/workflows/" in f or ".github/dependabot" in f: + categories["ci"] += 1 + + return categories + + +def build_summary_line(categories: dict[str, int], total: int) -> str: + """Build human-readable summary of changes.""" + parts = [] + if categories["src"] > 0: + parts.append(f"{categories['src']} source file(s)") + if categories["test"] > 0: + parts.append(f"{categories['test']} test file(s)") + if categories["doc"] > 0: + parts.append(f"{categories['doc']} doc file(s)") + if categories["ci"] > 0: + parts.append(f"{categories['ci']} CI file(s)") + + if parts: + return f"This PR touches {', '.join(parts)} across {total} file(s) total." + return f"This PR modifies {total} file(s)." + + +def main() -> int: + """Generate PR body from git history.""" + base_ref = os.environ["BASE_REF"] + + # Validate BASE_REF to prevent command injection + # Valid git refs: alphanumeric, hyphens, underscores, slashes, dots + # Reject anything suspicious + import re + + if not re.match(r"^[a-zA-Z0-9/_.-]+$", base_ref): + print(f"ERROR: Invalid BASE_REF format: {base_ref}", file=sys.stderr) + return 1 + + # Additional safety: reject refs that could be command injection attempts + if base_ref.startswith("-") or ".." in base_ref: + print(f"ERROR: Suspicious BASE_REF detected: {base_ref}", file=sys.stderr) + return 1 + + # Get merge base + merge_base = run_cmd(["git", "merge-base", f"origin/{base_ref}", "HEAD"]) + + # Get commit messages + commits = run_cmd(["git", "log", "--pretty=format:- %s", f"{merge_base}..HEAD"]) + + # Get changed files + diff_stat = run_cmd(["git", "diff", "--stat", f"{merge_base}..HEAD"]) + files = run_cmd(["git", "diff", "--name-only", f"{merge_base}..HEAD"]).split("\n") + files = [f for f in files if f] # Remove empty strings + + # Categorize + categories = categorize_files(files) + summary = build_summary_line(categories, len(files)) + + # Build body + body = f"""## Summary + +{summary} + +## Changes + +{commits} + +
+Diff stats +``` +{diff_stat} +``` + +
+ +## Testing + +- [ ] Tests pass locally (`python -m pytest tests/ -v`) +- [ ] Pre-commit hooks pass (`pre-commit run --all-files`) + +## Related Issues + + +""" + + print(body) + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/.github/workflows/auto-populate-pr.yml b/.github/workflows/auto-populate-pr.yml index b4c2996..6548c50 100644 --- a/.github/workflows/auto-populate-pr.yml +++ b/.github/workflows/auto-populate-pr.yml @@ -10,22 +10,28 @@ permissions: jobs: populate-body: name: Generate PR Body - runs-on: ubuntu-22.04 + runs-on: ubuntu-latest + timeout-minutes: 5 steps: - uses: actions/checkout@v4 with: fetch-depth: 0 + - uses: actions/setup-python@v5 + with: + python-version: "3.11" + - name: Fetch base branch run: git fetch origin ${{ github.event.pull_request.base.ref }} --depth=1 - - name: Generate and update PR body + - name: Check if PR body needs population + id: check env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} PR_NUMBER: ${{ github.event.pull_request.number }} - BASE_REF: ${{ github.event.pull_request.base.ref }} run: | - # Fetch the current PR body + set -euo pipefail + CURRENT_BODY=$(gh pr view "$PR_NUMBER" --json body -q '.body') # Strip HTML comments, headers, whitespace, checklist items @@ -39,71 +45,21 @@ jobs: if [[ -n "$STRIPPED" ]]; then echo "PR body already has custom content. Skipping auto-populate." - exit 0 - fi - - echo "PR body is empty or template-only. Generating content..." - - MERGE_BASE=$(git merge-base origin/"$BASE_REF" HEAD) - - # --- Collect commit messages --- - COMMITS=$(git log --pretty=format:"- %s" "$MERGE_BASE"..HEAD) - - # --- Collect changed files with stats --- - DIFF_STAT=$(git diff --stat "$MERGE_BASE"..HEAD) - FILE_LIST=$(git diff --name-only "$MERGE_BASE"..HEAD) - - # --- Categorize changes --- - TOTAL=$(echo "$FILE_LIST" | grep -c '.' || true) - SRC_CHANGES=$(echo "$FILE_LIST" | grep -c '^src/' || true) - TEST_CHANGES=$(echo "$FILE_LIST" | grep -c '^tests/' || true) - DOC_CHANGES=$(echo "$FILE_LIST" | grep -cE '^(.github/.*\.md|.*README)' || true) - CI_CHANGES=$(echo "$FILE_LIST" | grep -cE '^\.github/(workflows|dependabot)' || true) - - # --- Build summary line --- - SUMMARY_PARTS=() - [[ "$SRC_CHANGES" -gt 0 ]] && SUMMARY_PARTS+=("$SRC_CHANGES source file(s)") - [[ "$TEST_CHANGES" -gt 0 ]] && SUMMARY_PARTS+=("$TEST_CHANGES test file(s)") - [[ "$DOC_CHANGES" -gt 0 ]] && SUMMARY_PARTS+=("$DOC_CHANGES doc file(s)") - [[ "$CI_CHANGES" -gt 0 ]] && SUMMARY_PARTS+=("$CI_CHANGES CI file(s)") - - if [[ ${#SUMMARY_PARTS[@]} -gt 0 ]]; then - SUMMARY_LINE="This PR touches $(IFS=', '; echo "${SUMMARY_PARTS[*]}") across $TOTAL file(s) total." + echo "skip=true" >> "$GITHUB_OUTPUT" else - SUMMARY_LINE="This PR modifies $TOTAL file(s)." + echo "PR body is empty or template-only. Will generate content." + echo "skip=false" >> "$GITHUB_OUTPUT" fi - # --- Write PR body to a temp file --- - BODY_FILE=$(mktemp) - { - echo "## Summary" - echo "" - echo "$SUMMARY_LINE" - echo "" - echo "## Changes" - echo "" - echo "$COMMITS" - echo "" - echo "
" - echo "Diff stats" - echo "" - echo '```' - echo "$DIFF_STAT" - echo '```' - echo "" - echo "
" - echo "" - echo "## Testing" - echo "" - echo '- [ ] Tests pass locally (`python -m pytest tests/ -v`)' - echo '- [ ] Pre-commit hooks pass (`pre-commit run --all-files`)' - echo "" - echo "## Related Issues" - echo "" - echo "" - } > "$BODY_FILE" + - name: Generate and update PR body + if: steps.check.outputs.skip == 'false' + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + PR_NUMBER: ${{ github.event.pull_request.number }} + BASE_REF: ${{ github.event.pull_request.base.ref }} + run: | + set -euo pipefail - # Update the PR body from file - gh pr edit "$PR_NUMBER" --body-file "$BODY_FILE" - rm -f "$BODY_FILE" + BODY=$(python3 .github/scripts/generate_pr_body.py) + gh pr edit "$PR_NUMBER" --body "$BODY" echo "PR body has been auto-populated." diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 4c164fd..650bfb2 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -14,6 +14,7 @@ jobs: lint: name: Lint & Format runs-on: ubuntu-latest + timeout-minutes: 10 steps: - uses: actions/checkout@v4 @@ -21,10 +22,18 @@ jobs: with: python-version: "3.11" + - name: Cache pip packages + uses: actions/cache@v4 + with: + path: ~/.cache/pip + key: pip-${{ runner.os }}-3.11-${{ hashFiles('requirements-dev.txt') }} + restore-keys: | + pip-${{ runner.os }}-3.11- + - name: Install dependencies run: | pip install --upgrade pip - pip install pre-commit mypy bandit ruff + pip install -r requirements-dev.txt - name: Cache pre-commit hooks uses: actions/cache@v4 @@ -48,6 +57,7 @@ jobs: test: name: Test (Python ${{ matrix.python-version }}, ${{ matrix.os }}) runs-on: ${{ matrix.os }} + timeout-minutes: 15 needs: lint strategy: fail-fast: false @@ -61,20 +71,34 @@ jobs: with: python-version: ${{ matrix.python-version }} + - name: Cache pip packages + uses: actions/cache@v4 + with: + path: ~/.cache/pip + key: pip-${{ runner.os }}-${{ matrix.python-version }}-${{ hashFiles('requirements*.txt') }} + restore-keys: | + pip-${{ runner.os }}-${{ matrix.python-version }}- + pip-${{ runner.os }}- + - name: Install dependencies run: | pip install --upgrade pip - pip install -r requirements.txt + pip install -r requirements.txt requirements-dev.txt pip install -e . - - name: Run tests - run: python -m pytest tests/ -v - - name: Run tests with coverage run: | + set -euo pipefail python -m pytest tests/ \ -v \ --cov=template_project \ --cov-fail-under=90 \ --cov-report=term-missing \ --cov-report=xml + + - name: Upload coverage report + if: matrix.os == 'ubuntu-latest' && matrix.python-version == '3.11' + uses: actions/upload-artifact@v4 + with: + name: coverage-report + path: coverage.xml diff --git a/.github/workflows/dependency-review.yml b/.github/workflows/dependency-review.yml index c8b5542..35062b8 100644 --- a/.github/workflows/dependency-review.yml +++ b/.github/workflows/dependency-review.yml @@ -14,6 +14,7 @@ jobs: dependency-review: name: Review Dependencies runs-on: ubuntu-latest + timeout-minutes: 5 steps: - uses: actions/checkout@v4 diff --git a/.github/workflows/pr-body.yml b/.github/workflows/pr-body.yml index 17c391e..e712263 100644 --- a/.github/workflows/pr-body.yml +++ b/.github/workflows/pr-body.yml @@ -9,10 +9,13 @@ on: jobs: validate-body: name: Validate PR Body - runs-on: ubuntu-22.04 + runs-on: ubuntu-latest + timeout-minutes: 5 steps: - name: Check PR body is not empty run: | + set -euo pipefail + BODY=$(cat <<'BODY_EOF' ${{ github.event.pull_request.body }} BODY_EOF @@ -43,6 +46,8 @@ jobs: - name: Check for placeholder text run: | + set -euo pipefail + BODY=$(cat <<'BODY_EOF' ${{ github.event.pull_request.body }} BODY_EOF diff --git a/.github/workflows/pr-policy.yml b/.github/workflows/pr-policy.yml index 5f20b81..382437e 100644 --- a/.github/workflows/pr-policy.yml +++ b/.github/workflows/pr-policy.yml @@ -8,9 +8,11 @@ jobs: title-convention: name: Validate PR Title runs-on: ubuntu-latest + timeout-minutes: 5 steps: - name: Check conventional commit format run: | + set -euo pipefail TITLE="${{ github.event.pull_request.title }}" PATTERN="^(feat|fix|docs|doc|style|refactor|perf|test|build|ci|chore|revert)(\(.+\))?!?: .+" if [[ ! "$TITLE" =~ $PATTERN ]]; then @@ -32,9 +34,11 @@ jobs: label-check: name: Require Label runs-on: ubuntu-latest + timeout-minutes: 5 steps: - name: Check for at least one label run: | + set -euo pipefail LABEL_COUNT=$(echo '${{ toJson(github.event.pull_request.labels) }}' | jq 'length') if [[ "$LABEL_COUNT" -eq 0 ]]; then echo "::error::PR must have at least one label before merging." @@ -46,9 +50,11 @@ jobs: branch-naming: name: Validate Branch Name runs-on: ubuntu-latest + timeout-minutes: 5 steps: - name: Check branch naming convention run: | + set -euo pipefail BRANCH="${{ github.head_ref }}" PATTERN="^(feature|fix|bugfix|hotfix|docs|chore|refactor|test|ci|release|claude)/" if [[ ! "$BRANCH" =~ $PATTERN ]]; then diff --git a/requirements-dev.txt b/requirements-dev.txt new file mode 100644 index 0000000..b964292 --- /dev/null +++ b/requirements-dev.txt @@ -0,0 +1,7 @@ +bandit +mypy +# Development dependencies +pre-commit +pytest +pytest-cov +ruff From d03eab1141bf007e67712b9151b5489c3431360e Mon Sep 17 00:00:00 2001 From: dronefreak Date: Wed, 11 Feb 2026 17:24:19 +0100 Subject: [PATCH 6/6] feat: Update github workflows with timeouts; Add PR body script; Add requirements-dev.txt Signed-off-by: dronefreak --- .github/workflows/ci.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 650bfb2..8733ab2 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -83,7 +83,8 @@ jobs: - name: Install dependencies run: | pip install --upgrade pip - pip install -r requirements.txt requirements-dev.txt + pip install -r requirements.txt + pip install -r requirements-dev.txt pip install -e . - name: Run tests with coverage