diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 2fe438d..0b4bcf1 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -51,7 +51,7 @@ jobs: - name: Run tests run: | if [ "${{ matrix.python-version }}" = "3.11" ]; then - uv run pytest -q --no-testmon --cov --cov-report=xml --cov-report=term-missing -p no:cacheprovider + uv run pytest -q --no-testmon --cov=scripts --cov-report=xml --cov-report=term-missing -p no:cacheprovider else uv run pytest -q --no-testmon -p no:cacheprovider fi diff --git a/.gitignore b/.gitignore index 76bde4f..3f03a74 100644 --- a/.gitignore +++ b/.gitignore @@ -182,7 +182,6 @@ Thumbs.db !SECURITY.md !CODE_OF_CONDUCT.md !CONTRIBUTING.md -!TESTING_STREAMLIT.md *.backup *.py.backup @@ -233,7 +232,6 @@ docker-compose.override.yml # Stray root files 1 -/1 tasks/ task_channel/ diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 4550554..392ef8b 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -20,7 +20,7 @@ repos: - id: ruff-format name: Ruff Format - entry: uv run ruff format --quiet + entry: uv run --active ruff format --quiet language: system types: [python] pass_filenames: false @@ -28,7 +28,7 @@ repos: - id: ruff-check name: Ruff Lint (auto-fix) - entry: uv run ruff check --fix --exit-non-zero-on-fix --quiet + entry: uv run --active ruff check --fix --exit-non-zero-on-fix --quiet language: system types: [python] pass_filenames: false @@ -36,7 +36,7 @@ repos: - id: basedpyright name: BasedPyright (type check) - entry: uv run basedpyright + entry: uv run --active basedpyright language: system types: [python] pass_filenames: false diff --git a/copier.yml b/copier.yml index 3a5f6df..767e50a 100644 --- a/copier.yml +++ b/copier.yml @@ -62,9 +62,9 @@ package_name: default: "{{ project_name | lower | replace(' ', '_') | replace('-', '_') }}" help: Python package name (must be valid Python identifier) validator: >- - {% if not package_name.replace('_', '').isalnum() - or (package_name | length > 0 and package_name[0].isdigit()) %} - Package name must be a valid Python identifier (alphanumeric and underscores only; cannot start with a digit) + {% if not package_name.isidentifier() + or not package_name.replace('_', '').isalnum() %} + Package name must be a valid Python identifier (letters, digits, underscores; cannot start with a digit) {% endif %} project_description: @@ -158,6 +158,8 @@ current_year: when: false default: "{% now 'utc', '%Y' %}" +# Renders as a JSON array string, e.g. '["3.11", "3.12", "3.13"]'. In workflow templates, +# use fromJson() when passing to a matrix (e.g. matrix.python: ${{ fromJson(...) }}). github_actions_python_versions: type: str when: false @@ -226,7 +228,7 @@ _tasks: ' - command: uv lock - command: >- - uv sync --frozen --extra dev --extra test + uv sync --extra dev --extra test {%- if include_docs %} --extra docs{% endif -%} {%- if include_git_cliff %} --group changelog{% endif %} - command: uv run pre-commit install || true diff --git a/justfile b/justfile index 1eeca1f..479043d 100644 --- a/justfile +++ b/justfile @@ -123,14 +123,14 @@ test-failed-verbose: coverage: @uv run pytest tests/ \ --no-testmon \ - --cov \ + --cov=scripts \ --cov-report=term-missing \ --cov-report=html \ --cov-report=xml # Test command matching GitHub CI (3.11 matrix leg in .github/workflows/tests.yml) test-ci: - @uv run pytest -q --no-testmon --cov --cov-report=xml --cov-report=term-missing -p no:cacheprovider + @uv run pytest -q --no-testmon --cov=scripts --cov-report=xml --cov-report=term-missing -p no:cacheprovider # Full tests.yml matrix (3.11 with coverage; 3.12/3.13 with pytest -q only). # 3.11 uses the default project .venv (same as `test-ci`). 3.12/3.13 use @@ -144,7 +144,7 @@ test-ci-matrix: echo "=== Python 3.11 + coverage (tests.yml matrix) ===" unset UV_PROJECT_ENVIRONMENT uv sync --frozen --extra dev --extra test --python 3.11 - uv run pytest -q --no-testmon --cov --cov-report=xml --cov-report=term-missing + uv run pytest -q --no-testmon --cov=scripts --cov-report=xml --cov-report=term-missing for py in 3.12 3.13; do echo "=== Python ${py} (tests.yml matrix) ===" suffix="${py//./}" @@ -237,12 +237,32 @@ publish: # ------------------------------------------------------------------------- # Install all package dependencies # ------------------------------------------------------------------------- +# Ensures `uv` is a user-level tool (not installed into the project venv). On Windows, install +# uv from https://docs.astral.sh/uv/ if bash/curl are unavailable. install: - @python -m pip install --upgrade pip - @python -m pip install --upgrade uv - @just sync - @just precommit-install + #!/usr/bin/env bash + set -euo pipefail + if ! command -v uv >/dev/null 2>&1; then + if command -v curl >/dev/null 2>&1; then + curl -LsSf https://astral.sh/uv/install.sh | sh + elif command -v wget >/dev/null 2>&1; then + wget -qO- https://astral.sh/uv/install.sh | sh + else + echo "Install uv from https://docs.astral.sh/uv/ (Windows: PowerShell installer)." >&2 + exit 1 + fi + if [ -f "${HOME}/.local/bin/env" ]; then + # shellcheck source=/dev/null + . "${HOME}/.local/bin/env" + fi + fi + command -v uv >/dev/null 2>&1 || { + echo "uv not found on PATH after install" >&2 + exit 1 + } + just sync + just precommit-install # One-command developer onboarding: sync deps, register hooks, run diagnostics bootstrap: diff --git a/pyproject.toml b/pyproject.toml index 5e638e6..43e3957 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -82,8 +82,9 @@ select = [ "T20", # flake8-print — discourage print() in non-test code ] +# E501 is intentionally ignored — line length is enforced by ruff format, not ruff check ignore = [ - "E501", # line too long (handled by formatter) + "E501", ] [tool.ruff.lint.pydocstyle] diff --git a/template/.github/workflows/ci.yml.jinja b/template/.github/workflows/ci.yml.jinja index 7777570..9c4cd16 100644 --- a/template/.github/workflows/ci.yml.jinja +++ b/template/.github/workflows/ci.yml.jinja @@ -118,7 +118,7 @@ jobs: run: uv sync --frozen --extra dev --extra test{% if include_docs %} --extra docs{% endif %} - name: Run tests - run: uv run pytest -q --no-testmon --cov --cov-report=xml --cov-report=term-missing -p no:cacheprovider + run: uv run pytest -q --no-testmon --cov={{ package_name }} --cov-report=xml --cov-report=term-missing -p no:cacheprovider - name: Upload coverage to Codecov uses: codecov/codecov-action@v6 diff --git a/template/.gitignore b/template/.gitignore index 76bde4f..3f03a74 100644 --- a/template/.gitignore +++ b/template/.gitignore @@ -182,7 +182,6 @@ Thumbs.db !SECURITY.md !CODE_OF_CONDUCT.md !CONTRIBUTING.md -!TESTING_STREAMLIT.md *.backup *.py.backup @@ -233,7 +232,6 @@ docker-compose.override.yml # Stray root files 1 -/1 tasks/ task_channel/ diff --git a/template/.pre-commit-config.yaml b/template/.pre-commit-config.yaml index 4550554..392ef8b 100644 --- a/template/.pre-commit-config.yaml +++ b/template/.pre-commit-config.yaml @@ -20,7 +20,7 @@ repos: - id: ruff-format name: Ruff Format - entry: uv run ruff format --quiet + entry: uv run --active ruff format --quiet language: system types: [python] pass_filenames: false @@ -28,7 +28,7 @@ repos: - id: ruff-check name: Ruff Lint (auto-fix) - entry: uv run ruff check --fix --exit-non-zero-on-fix --quiet + entry: uv run --active ruff check --fix --exit-non-zero-on-fix --quiet language: system types: [python] pass_filenames: false @@ -36,7 +36,7 @@ repos: - id: basedpyright name: BasedPyright (type check) - entry: uv run basedpyright + entry: uv run --active basedpyright language: system types: [python] pass_filenames: false diff --git a/template/justfile.jinja b/template/justfile.jinja index 9550867..bfd752c 100644 --- a/template/justfile.jinja +++ b/template/justfile.jinja @@ -131,7 +131,7 @@ coverage: # Test command matching GitHub CI (.github/workflows/ci.yml test job) test-ci: - @uv run pytest -q --no-testmon --cov --cov-report=xml --cov-report=term-missing -p no:cacheprovider + @uv run pytest -q --no-testmon --cov={{ package_name }} --cov-report=xml --cov-report=term-missing -p no:cacheprovider # ------------------------------------------------------------------------- # Pre-commit @@ -230,12 +230,32 @@ publish: # ------------------------------------------------------------------------- # Install all package dependencies # ------------------------------------------------------------------------- +# Ensures `uv` is a user-level tool (not installed into the project venv). On Windows, install +# uv from https://docs.astral.sh/uv/ if bash/curl are unavailable. install: - @python -m pip install --upgrade pip - @python -m pip install --upgrade uv - @just sync - @just precommit-install + #!/usr/bin/env bash + set -euo pipefail + if ! command -v uv >/dev/null 2>&1; then + if command -v curl >/dev/null 2>&1; then + curl -LsSf https://astral.sh/uv/install.sh | sh + elif command -v wget >/dev/null 2>&1; then + wget -qO- https://astral.sh/uv/install.sh | sh + else + echo "Install uv from https://docs.astral.sh/uv/ (Windows: PowerShell installer)." >&2 + exit 1 + fi + if [ -f "${HOME}/.local/bin/env" ]; then + # shellcheck source=/dev/null + . "${HOME}/.local/bin/env" + fi + fi + command -v uv >/dev/null 2>&1 || { + echo "uv not found on PATH after install" >&2 + exit 1 + } + just sync + just precommit-install # One-command developer onboarding: sync deps, register hooks, run diagnostics bootstrap: @@ -365,3 +385,37 @@ release BUMP_TYPE="patch": @echo "✅ Release complete!" @git describe --tags --abbrev=0 + +# ------------------------------------------------------------------------- +# Repo automation +# ------------------------------------------------------------------------- + +# Generate repo freshness dashboard + JSON artifacts +freshness: + @uv run python scripts/repo_file_freshness.py + +# Validate root/template sync map and parity checks +sync-check: + @uv run python scripts/check_root_template_sync.py + +# Print a conventional PR title + PR body (template + git log) for pr-policy compliance +pr-draft: + @uv run python scripts/pr_commit_policy.py draft + +# ------------------------------------------------------------------------- +# SDLC: Task management +# ------------------------------------------------------------------------- + +# Validate a task YAML against Definition of Ready +dor-check TASK_ID: + python3 .claude/skills/sdlc-workflow/scripts/validate_dor.py tasks/{{TASK_ID}}.yaml + +# List all tasks and their statuses +tasks: + @echo "Task ID Status Title" + @echo "---------- ---------- -----" + @python3 -c "import yaml; from pathlib import Path; [print(f\"{d['task_id']:<14}{d['status']:<14}{d['title']}\") for p in sorted(Path('tasks').glob('TASK_*.yaml')) if (d := yaml.safe_load(p.read_text()))]" + +# Run pre-flight checks before starting SDLC pipeline +preflight TASK_ID: + bash .claude/skills/sdlc-workflow/scripts/preflight.sh {{TASK_ID}} diff --git a/template/pyproject.toml.jinja b/template/pyproject.toml.jinja index 81eacc5..ab1c76a 100644 --- a/template/pyproject.toml.jinja +++ b/template/pyproject.toml.jinja @@ -119,8 +119,9 @@ select = [ "T20", # flake8-print — discourage print() in non-test code ] +# E501 is intentionally ignored — line length is enforced by ruff format, not ruff check ignore = [ - "E501", # line too long (handled by formatter) + "E501", ] [tool.ruff.lint.pydocstyle] diff --git a/tests/integration/test_template.py b/tests/integration/test_template.py index e3f13fe..d2708b7 100644 --- a/tests/integration/test_template.py +++ b/tests/integration/test_template.py @@ -394,6 +394,28 @@ def test_package_name_validator_rejects_leading_digit(tmp_path: Path) -> None: assert proc.returncode != 0, "copier should reject package_name starting with a digit" +def test_package_name_validator_rejects_non_identifier(tmp_path: Path) -> None: + """Hyphenated ``package_name`` values must fail Copier validation (not a Python identifier).""" + test_dir = tmp_path / "bad_hyphen_pkg" + proc = run_command( + [ + "copier", + "copy", + "--vcs-ref", + "HEAD", + TEMPLATE_GIT_SRC, + str(test_dir), + "--trust", + "--defaults", + "--skip-tasks", + "--data", + "package_name=my-bad-pkg", + ], + check=False, + ) + assert proc.returncode != 0, "copier should reject package_name that is not a valid identifier" + + def test_computed_values_not_recorded_in_answers_file(tmp_path: Path) -> None: """Questions with ``when: false`` must not be stored in the answers file.""" test_dir = tmp_path / "computed_answers" @@ -1572,14 +1594,14 @@ def test_ci_workflow_aligns_with_just_ci(tmp_path: Path) -> None: assert "uv run ruff format --check src tests" in workflow assert "uv run ruff check src tests" in workflow assert ( - "uv run pytest -q --no-testmon --cov --cov-report=xml --cov-report=term-missing -p no:cacheprovider" + "uv run pytest -q --no-testmon --cov=ci_just_align --cov-report=xml --cov-report=term-missing -p no:cacheprovider" in workflow ) justfile = (test_dir / "justfile").read_text(encoding="utf-8") assert "test-ci:" in justfile assert ( - "pytest -q --no-testmon --cov --cov-report=xml --cov-report=term-missing -p no:cacheprovider" + "pytest -q --no-testmon --cov=ci_just_align --cov-report=xml --cov-report=term-missing -p no:cacheprovider" in justfile ) assert "@just test-ci" in justfile