diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml new file mode 100644 index 0000000..ff08924 --- /dev/null +++ b/.github/workflows/ci.yaml @@ -0,0 +1,142 @@ +# yaml-language-server: $schema=https://json.schemastore.org/github-workflow.json + +name: ci + +on: + push: + branches: + - main + pull_request: + +permissions: + contents: read + id-token: write + pull-requests: write + +jobs: + ci: + name: ${{ matrix.name }} + runs-on: ubuntu-24.04 + concurrency: + group: ${{ github.workflow }}-${{ matrix.name }}-${{ github.event.pull_request.number }} + cancel-in-progress: true + strategy: + fail-fast: false + matrix: + include: + - name: test + task: test + install: true + run_on_push: true + annotations_pytest: true + - name: pre-commit + task: install + install: true + pre_commit: true + - name: template - build + task: build + template: true + buildx: true + - name: template - check - format + task: check:format + template: true + install: true + annotations_ruff: true + - name: template - check - lint + task: check:lint + template: true + install: true + annotations_ruff: true + - name: template - check - types + task: check:types + template: true + install: true + annotations_mypy: true + - name: template - test - 3.10 + task: test + template: true + install: true + run_on_push: true + python: "3.10" + - name: template - test - 3.11 + task: test + template: true + install: true + run_on_push: true + python: "3.11" + - name: template - test - 3.12 + task: test + template: true + install: true + run_on_push: true + python: "3.12" + - name: template - test - 3.13 + task: test + template: true + install: true + run_on_push: true + python: "3.13" + - name: template - test - 3.14 + task: test + template: true + install: true + run_on_push: true + python: "3.14" + annotations_pytest: true + pytest_coverage: true + pytest_coverage_xml: tests/example-project/coverage.xml + pytest_coverage_junit: tests/example-project/pytest.xml + - name: template - pre-commit + task: install + template: true + install: true + pre_commit: true + steps: + - name: lock template project + if: (matrix.template || false) && (github.event_name == 'pull_request' || matrix.run_on_push) + uses: juftin/actions/taskfile@v1 + with: + task: lock + task-args: --taskfile tests/example-project/Taskfile.yaml + checkout: true + fetch-depth: ${{ matrix.pre_commit && '0' || '1' }} + setup-uv: true + github-token: ${{ secrets.GITHUB_TOKEN }} + - name: install template project + if: matrix.template && matrix.install && (github.event_name == 'pull_request' || matrix.run_on_push) + uses: juftin/actions/taskfile@v1 + with: + task: install + task-args: --taskfile tests/example-project/Taskfile.yaml + setup-task: false + setup-uv: false + - name: ${{ matrix.name }} + if: github.event_name == 'pull_request' || matrix.run_on_push + uses: juftin/actions/taskfile@v1 + with: + task: ${{ matrix.task }} + task-args: ${{ matrix.template && '--taskfile tests/example-project/Taskfile.yaml' || '' }} + checkout: ${{ !matrix.template }} + install: ${{ matrix.install || false }} + fetch-depth: ${{ matrix.pre_commit && '0' || '1' }} + setup-task: ${{ !matrix.template }} + setup-uv: ${{ !matrix.template }} + buildx: ${{ matrix.buildx || false }} + github-token: ${{ secrets.GITHUB_TOKEN }} + annotations-ruff: ${{ matrix.annotations_ruff || false }} + annotations-mypy: ${{ matrix.annotations_mypy || false }} + annotations-pytest: ${{ matrix.annotations_pytest || false }} + pytest-coverage: ${{ matrix.pytest_coverage && github.event_name == 'pull_request' && !github.event.pull_request.head.repo.fork || false }} + pytest-coverage-xml: ${{ matrix.pytest_coverage_xml || 'coverage.xml' }} + pytest-coverage-junit: ${{ matrix.pytest_coverage_junit || 'pytest.xml' }} + env: + UV_PYTHON: ${{ matrix.python || '' }} + - name: pre-commit + if: matrix.pre_commit && github.event_name == 'pull_request' + run: | + uv tool run --isolated \ + --with pre-commit-uv \ + pre-commit run \ + --color always \ + --from-ref ${{ github.event.pull_request.base.sha }} \ + --to-ref ${{ github.event.pull_request.head.sha }} diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml deleted file mode 100644 index a20ab15..0000000 --- a/.github/workflows/test.yaml +++ /dev/null @@ -1,26 +0,0 @@ -# yaml-language-server: $schema=https://www.schemastore.org/github-workflow.json - -name: test - -on: - push: - branches: - - main - pull_request: - -jobs: - test: - runs-on: ubuntu-latest - permissions: - contents: read - id-token: write - pull-requests: write - steps: - - name: test - uses: juftin/actions/taskfile@v1 - with: - checkout: true - setup-uv: true - task: test - install: true - github-token: ${{ secrets.GITHUB_TOKEN }} diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 803e49b..71d28f4 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -40,7 +40,11 @@ repos: description: Runs Prettier Code Formatter entry: prettier --write --ignore-unknown args: ["--tab-width=4"] - exclude: \.github/\.copier-answers\.yaml$ + exclude: | + (?x)( + \.github/\.copier-answers\.yaml$| + ^copier\.yaml$ + ) language: node types: - text diff --git a/Taskfile.yaml b/Taskfile.yaml index 12b11ce..c652f95 100644 --- a/Taskfile.yaml +++ b/Taskfile.yaml @@ -6,21 +6,27 @@ env: UV_LOCKED: 1 tasks: - ####################################### + ######################## + # DEFAULT TASK + ######################## default: - desc: Install Project, List Tasks + desc: List Available Tasks cmds: - - task --list-all - deps: [sync] - ####################################### + - task --list-all --sort alphanumeric + silent: true + ######################## + # INSTALL AND DEPENDENCIES + ######################## install: desc: Install Project + Dev Dependencies cmds: - task: pre-commit-install - task: sync - ####################################### + ######################## + # TESTING + ######################## test: - desc: Run Tests + desc: Run Copier Tests cmds: - uv run -- pytest tests/test_copier.py {{.CLI_ARGS}} deps: @@ -30,33 +36,39 @@ tasks: CLI_ARGS: --quiet env: UV_PYTHON: "{{.UV_PYTHON}}" - ####################################### + ######################## + # DOCUMENTATION AND COMMANDS + ######################## docs: - desc: Generate Documentation + desc: Run Documentation Site cmds: - uv run --group docs -- mkdocs {{.CLI_ARGS | default "serve"}} ####################################### run: - desc: Run Command within Project (requires "--") + desc: Run Project Command (requires "--") interactive: true cmds: - uv run -- {{.CLI_ARGS}} - ####################################### + ######################## + # DEPENDENCY MANAGEMENT + ######################## lock: - desc: Regenerate the Project Lockfile + desc: Regenerate Project Lockfile cmds: - uv lock env: UV_LOCKED: "0" - ####################################### + ######################## + # GENERATED PROJECT FIXTURE + ######################## example-project: - desc: Regenerate the example project for testing + desc: Regenerate Example Project cmds: - rm -rf tests/example-project - uv run -- copier copy --vcs-ref=HEAD --defaults --data-file tests/answers.yml . {{.CLI_ARGS | default "tests/example-project"}} - ####################################### + ######################## # INTERNAL TASKS - ####################################### + ######################## sync: desc: Install Project Dependencies internal: true diff --git a/docs/contributing.md b/docs/contributing.md index 03b064c..a8f68db 100644 --- a/docs/contributing.md +++ b/docs/contributing.md @@ -18,7 +18,7 @@ manager) and **[task]** (task runner). Once both are installed, you can use sh -c "$(curl --location https://taskfile.dev/install.sh)" -- -d ``` -3. Install project dependencies and list available tasks +3. Install the project development environment ```shell task install @@ -34,18 +34,15 @@ manager) and **[task]** (task runner). Once both are installed, you can use ### Task Cheat Sheet -| Command Description | Command | Notes | -| ------------------- | ------------------- | --------------------------------------- | -| Install Project | `task install` | Installs project and dev dependencies | -| Run Tests | `task test` | Runs tests with `pytest` | -| Run Linting | `task lint` | Lints code with `ruff` | -| Fix Code Issues | `task fix` | Formats and auto-fixes code with `ruff` | -| Run Formatting | `task fmt` | Formats code with `ruff` | -| Run Type Checking | `task check` | Runs static analysis with `mypy` | -| Build Project | `task build` | Builds project artifacts | -| Update Dependencies | `task lock` | Regenerates project lockfile | -| Serve Documentation | `task docs` | Serves docs with `mkdocs` | -| Run Commands | `task run -- ` | Runs arbitrary commands | +| Command Description | Command | Notes | +| ---------------------------------- | ---------------------- | ------------------------------------------ | +| List Available Tasks | `task` | Lists tasks in alphanumeric order | +| Install Project + Dev Dependencies | `task install` | Installs dependencies and pre-commit hooks | +| Run Copier Tests | `task test` | Runs the Copier test suite | +| Run Documentation Site | `task docs` | Runs MkDocs | +| Run a Project Command | `task run -- ` | Runs a command in the project environment | +| Regenerate Project Lockfile | `task lock` | Regenerates `uv.lock` | +| Regenerate Example Project | `task example-project` | Replaces the checked-in generated fixture | ### Task Explanation diff --git a/template/.github/workflows/ci.yaml b/template/.github/workflows/ci.yaml new file mode 100644 index 0000000..5403b2f --- /dev/null +++ b/template/.github/workflows/ci.yaml @@ -0,0 +1,95 @@ +# yaml-language-server: $schema=https://json.schemastore.org/github-workflow.json + +name: ci + +on: + push: + branches: + - main + pull_request: + +permissions: + contents: read + id-token: write + pull-requests: write + +jobs: + ci: + name: ${{"{{"}} matrix.name {{"}}"}} + runs-on: ubuntu-24.04 + concurrency: + group: ${{"{{"}} github.workflow {{"}}"}}-${{"{{"}} matrix.name {{"}}"}}-${{"{{"}} github.event.pull_request.number {{"}}"}} + cancel-in-progress: true + strategy: + fail-fast: false + matrix: + include: + - name: build + task: build + - name: check - format + task: check:format + install: true + annotations_ruff: true + - name: check - lint + task: check:lint + install: true + annotations_ruff: true + - name: check - types + task: check:types + install: true + annotations_mypy: true + - name: test - 3.10 + task: test + install: true + run_on_push: true + python: "3.10" + - name: test - 3.11 + task: test + install: true + run_on_push: true + python: "3.11" + - name: test - 3.12 + task: test + install: true + run_on_push: true + python: "3.12" + - name: test - 3.13 + task: test + install: true + run_on_push: true + python: "3.13" + - name: test - 3.14 + task: test + install: true + run_on_push: true + python: "3.14" + - name: pre-commit + task: install + install: true + steps: + - name: ${{"{{"}} matrix.name {{"}}"}} + if: github.event_name == 'pull_request' || matrix.run_on_push + uses: juftin/actions/taskfile@v1 + with: + task: ${{"{{"}} matrix.task {{"}}"}} + checkout: true + install: ${{"{{"}} matrix.install || false {{"}}"}} + fetch-depth: ${{"{{"}} matrix.name == 'pre-commit' && '0' || '1' {{"}}"}} + setup-uv: true + buildx: ${{"{{"}} matrix.name == 'build' {{"}}"}} + github-token: ${{"{{"}} secrets.GITHUB_TOKEN {{"}}"}} + annotations-ruff: ${{"{{"}} matrix.annotations_ruff || false {{"}}"}} + annotations-mypy: ${{"{{"}} matrix.annotations_mypy || false {{"}}"}} + annotations-pytest: ${{"{{"}} matrix.python == '{{ default_python_version }}' {{"}}"}} + pytest-coverage: ${{"{{"}} matrix.python == '{{ default_python_version }}' && github.event_name == 'pull_request' && !github.event.pull_request.head.repo.fork {{"}}"}} + env: + UV_PYTHON: ${{"{{"}} matrix.python || '' {{"}}"}} + - name: pre-commit + if: matrix.name == 'pre-commit' && github.event_name == 'pull_request' + run: | + uv tool run --isolated \ + --with pre-commit-uv \ + pre-commit run \ + --color always \ + --from-ref ${{"{{"}} github.event.pull_request.base.sha {{"}}"}} \ + --to-ref ${{"{{"}} github.event.pull_request.head.sha {{"}}"}} diff --git a/template/.github/workflows/lint.yaml b/template/.github/workflows/lint.yaml deleted file mode 100644 index 96e5d90..0000000 --- a/template/.github/workflows/lint.yaml +++ /dev/null @@ -1,37 +0,0 @@ -# yaml-language-server: $schema=https://www.schemastore.org/github-workflow.json - -name: lint - -on: - pull_request: - -jobs: - lint: - runs-on: ubuntu-latest - steps: - - name: lint - id: lint - uses: juftin/actions/taskfile@v1 - continue-on-error: true - with: - checkout: true - setup-uv: true - task: lint - install: true - github-token: ${{"{{"}} secrets.GITHUB_TOKEN {{"}}"}} - annotations-ruff: true - - name: check - id: check - uses: juftin/actions/taskfile@v1 - continue-on-error: true - with: - task: check - setup-task: false - annotations-mypy: true - - name: Raise Errors For Failures - if: | - steps.lint.outcome != 'success' || steps.check.outcome != 'success' - run: | - echo "Lint: ${{"{{"}} steps.lint.outcome {{"}}"}}" - echo "Check: ${{"{{"}} steps.check.outcome {{"}}"}}" - exit 1 diff --git a/template/.github/workflows/test.yaml b/template/.github/workflows/test.yaml deleted file mode 100644 index b34fb20..0000000 --- a/template/.github/workflows/test.yaml +++ /dev/null @@ -1,51 +0,0 @@ -# yaml-language-server: $schema=https://www.schemastore.org/github-workflow.json - -name: test - -on: - push: - branches: - - {{ default_branch }} - paths: - - src/{{ project_slug }}/** - - pyproject.toml - - uv.lock - - .github/workflows/tests.yaml - pull_request: - paths: - - src/{{ project_slug }}/** - - pyproject.toml - - uv.lock - - .github/workflows/tests.yaml - schedule: - - cron: 0 12 1 * * - -jobs: - test: - runs-on: ubuntu-latest - strategy: - fail-fast: true - matrix: - include: - - { name: Python 3.13, python: "3.13" } - - { name: Python 3.12, python: "3.12" } - - { name: Python 3.11, python: "3.11" } - - { name: Python 3.10, python: "3.10" } - - { name: Python 3.9, python: "3.9" } - permissions: - contents: read - id-token: write - pull-requests: write - steps: - - name: test - uses: juftin/actions/taskfile@v1 - with: - checkout: true - setup-uv: true - task: test - install: true - github-token: ${{"{{"}} secrets.GITHUB_TOKEN {{"}}"}} - annotations-pytest: ${{"{{"}} matrix.python == '{{ default_python_version }}' && true || false {{"}}"}} - pytest-coverage: ${{"{{"}} matrix.python == '{{ default_python_version }}' && github.event_name == 'pull_request' && true || false {{"}}"}} - env: - UV_PYTHON: ${{"{{"}} matrix.python {{"}}"}} diff --git a/template/.github/workflows/{% if build_docker_image %}docker.yaml{% endif %} b/template/.github/workflows/{% if build_docker_image %}docker.yaml{% endif %} deleted file mode 100644 index 6419c96..0000000 --- a/template/.github/workflows/{% if build_docker_image %}docker.yaml{% endif %} +++ /dev/null @@ -1,25 +0,0 @@ -# yaml-language-server: $schema=https://www.schemastore.org/github-workflow.json - -name: docker - -on: - pull_request: - paths: - - src/{{ project_slug }}/** - - pyproject.toml - - .github/workflows/docker.yaml - - Dockerfile - - uv.lock - -jobs: - docker: - runs-on: ubuntu-latest - steps: - - name: docker - uses: juftin/actions/taskfile@v1 - with: - checkout: true - setup-uv: true - task: docker - buildx: true - github-token: ${{"{{"}} secrets.GITHUB_TOKEN {{"}}"}} diff --git a/template/Taskfile.yaml b/template/Taskfile.yaml index 00cadb1..9dc0c4e 100644 --- a/template/Taskfile.yaml +++ b/template/Taskfile.yaml @@ -5,23 +5,28 @@ version: "3" env: UV_LOCKED: 1 vars: - SOURCE_CODE: src/{{ project_slug }} DOCKER_REPO: {{ github_user }}/{{ package_name }} tasks: - ####################################### + ######################## + # DEFAULT TASK + ######################## default: - desc: Install Project, List Tasks + desc: List Available Tasks cmds: - - task --list-all - deps: [sync] - ####################################### + - task --list-all --sort alphanumeric + silent: true + ######################## + # INSTALL AND DEPENDENCIES + ######################## install: desc: Install Project + Dev Dependencies cmds: - task: pre-commit-install - task: sync - ####################################### + ######################## + # TESTING + ######################## test: desc: Run Tests cmds: @@ -35,52 +40,72 @@ tasks: UV_PYTHON: "{{"{{"}}.UV_PYTHON{{"}}"}}" ####################################### test:matrix: - desc: Run Tests with different Python versions + desc: Run Tests across Python Versions cmds: - for: matrix: - PYTHON: ["3.9", "3.10", "3.11", "3.12", "3.13"] + PYTHON: ["3.10", "3.11", "3.12", "3.13", "3.14"] task: test vars: UV_PYTHON: "{{"{{"}}.ITEM.PYTHON{{"}}"}}" - ####################################### - lint: - desc: Run Code Linters + ######################## + # CODE QUALITY CHECKS + ######################## + check:format: + desc: Code Quality Check - Formatting cmds: - - uv run -- ruff check {{"{{"}}.CLI_ARGS | default .ROOT_DIR{{"}}"}} - task: fmt vars: CLI_ARGS: --check - deps: [sync] ####################################### - fmt: - desc: Run Code Formatters + check:lint: + desc: Code Quality Check - Linting cmds: - - uv run -- ruff format {{"{{"}}.CLI_ARGS | default .ROOT_DIR{{"}}"}} + - uv run -- ruff check {{"{{"}}.CLI_ARGS | default .ROOT_DIR{{"}}"}} deps: [sync] ####################################### - fix: - desc: Fix Code with Linters and Formatters + check:style: + desc: Code Quality Check - Formatting + Linting cmds: - - task: fmt - - uv run -- ruff check --fix {{"{{"}}.CLI_ARGS | default .ROOT_DIR{{"}}"}} - deps: [sync] + - task: check:format + - task: check:lint ####################################### - check: - desc: Run Static Type Checkers + check:types: + desc: Code Quality Check - Type Checking cmds: - | uv run -- \ mypy \ --ignore-missing-imports \ - {{"{{"}}.CLI_ARGS | default .SOURCE_CODE{{"}}"}} + {{"{{"}}.CLI_ARGS | default (printf "%s/src" .ROOT_DIR){{"}}"}} status: - '[$(uv tree --package mypy) != ""]' - requires: - vars: [SOURCE_CODE] ####################################### + check: + desc: Code Quality Check - Formatting + Linting + Type Checking + cmds: + - task: check:style + - task: check:types + ######################## + # CODE QUALITY FIXES + ######################## + fmt: + desc: Code Quality Auto-Fix - Formatting + cmds: + - uv run -- ruff format {{"{{"}}.CLI_ARGS | default .ROOT_DIR{{"}}"}} + deps: [sync] + ####################################### + fix: + desc: Code Quality Auto-Fix - Formatting + Linting + cmds: + - task: fmt + - uv run -- ruff check --fix {{"{{"}}.CLI_ARGS | default .ROOT_DIR{{"}}"}} + deps: [sync] + ######################## + # BUILD AND PUBLISH + ######################## dist: - desc: Build the Project Artifacts + desc: Build Distribution Artifacts cmds: - cmd: rm -rf {{"{{"}}.ROOT_DIR{{"}}"}}/dist - cmd: uv build @@ -103,13 +128,13 @@ tasks: - test ! -f {{"{{"}}.ROOT_DIR{{"}}"}}/{{"{{"}}.DOCKERFILE | default "Dockerfile"{{"}}"}} ####################################### build: - desc: Build the Project Artifacts + desc: Build Project Artifacts cmds: - task: dist - task: docker ####################################### publish: - desc: Publish the Project Artifacts + desc: Publish Docker Image cmds: - task: docker vars: @@ -118,20 +143,22 @@ tasks: vars: [CI, RELEASE_VERSION] status: - test ! -f {{"{{"}}.ROOT_DIR{{"}}"}}/{{"{{"}}.DOCKERFILE | default "Dockerfile"{{"}}"}} - ####################################### + ######################## + # DOCUMENTATION AND COMMANDS + ######################## docs: - desc: Generate Documentation + desc: Run Documentation Site cmds: - uv run --group docs -- mkdocs {{"{{"}}.CLI_ARGS | default "serve"{{"}}"}} ####################################### run: - desc: Run Command within Project (requires "--") + desc: Run Project Command (requires "--") interactive: true cmds: - uv run -- {{"{{"}}.CLI_ARGS{{"}}"}} ####################################### docker-run: - desc: Run Command within Docker Container (supports "--" for command) + desc: Run Docker Command (requires "--") interactive: true cmds: - | @@ -147,16 +174,18 @@ tasks: - test ! -f {{"{{"}}.ROOT_DIR{{"}}"}}/{{"{{"}}.DOCKERFILE | default "Dockerfile"{{"}}"}} requires: vars: [DOCKER_REPO] - ####################################### + ######################## + # DEPENDENCY MANAGEMENT + ######################## lock: - desc: Regenerate the Project Lockfile + desc: Regenerate Project Lockfile cmds: - uv lock env: UV_LOCKED: "0" - ####################################### + ######################## # INTERNAL TASKS - ####################################### + ######################## sync: desc: Install Project Dependencies internal: true diff --git a/template/docs/contributing.md b/template/docs/contributing.md index 03b064c..7a74eed 100644 --- a/template/docs/contributing.md +++ b/template/docs/contributing.md @@ -18,7 +18,7 @@ manager) and **[task]** (task runner). Once both are installed, you can use sh -c "$(curl --location https://taskfile.dev/install.sh)" -- -d ``` -3. Install project dependencies and list available tasks +3. Install the project development environment ```shell task install @@ -34,18 +34,27 @@ manager) and **[task]** (task runner). Once both are installed, you can use ### Task Cheat Sheet -| Command Description | Command | Notes | -| ------------------- | ------------------- | --------------------------------------- | -| Install Project | `task install` | Installs project and dev dependencies | -| Run Tests | `task test` | Runs tests with `pytest` | -| Run Linting | `task lint` | Lints code with `ruff` | -| Fix Code Issues | `task fix` | Formats and auto-fixes code with `ruff` | -| Run Formatting | `task fmt` | Formats code with `ruff` | -| Run Type Checking | `task check` | Runs static analysis with `mypy` | -| Build Project | `task build` | Builds project artifacts | -| Update Dependencies | `task lock` | Regenerates project lockfile | -| Serve Documentation | `task docs` | Serves docs with `mkdocs` | -| Run Commands | `task run -- ` | Runs arbitrary commands | +| Command Description | Command | Notes | +| ------------------------------- | --------------------------- | ----------------------------------------------- | +| List Available Tasks | `task` | Lists tasks in alphanumeric order | +| Install Project + Dev Dependencies | `task install` | Installs dependencies and pre-commit hooks | +| Run Tests | `task test` | Runs tests with `pytest` | +| Run Tests across Python Versions | `task test:matrix` | Runs tests on Python 3.10 through 3.14 | +| Code Quality Check - Formatting | `task check:format` | Checks formatting with `ruff` | +| Code Quality Check - Linting | `task check:lint` | Checks linting with `ruff` | +| Code Quality Check - Formatting + Linting | `task check:style` | Runs formatting and linting checks | +| Code Quality Check - Type Checking | `task check:types` | Checks types with `mypy` | +| Code Quality Check - Formatting + Linting + Type Checking | `task check` | Runs all code checks | +| Code Quality Auto-Fix - Formatting | `task fmt` | Formats code with `ruff` | +| Code Quality Auto-Fix - Formatting + Linting | `task fix` | Formats code and applies `ruff` fixes | +| Build Distribution Artifacts | `task dist` | Builds Python distribution artifacts | +| Build Docker Image | `task docker` | Builds the Docker image when a Dockerfile exists | +| Build Project Artifacts | `task build` | Builds distribution artifacts and Docker image | +| Publish Docker Image | `task publish` | Pushes the Docker image in CI | +| Run Documentation Site | `task docs` | Runs MkDocs | +| Run Project Command | `task run -- ` | Runs a command in the project environment | +| Run Docker Command | `task docker-run -- ` | Runs a command in the Docker image | +| Regenerate Project Lockfile | `task lock` | Regenerates `uv.lock` | ### Task Explanation diff --git a/tests/example-project/.github/workflows/ci.yaml b/tests/example-project/.github/workflows/ci.yaml new file mode 100644 index 0000000..30fc29c --- /dev/null +++ b/tests/example-project/.github/workflows/ci.yaml @@ -0,0 +1,95 @@ +# yaml-language-server: $schema=https://json.schemastore.org/github-workflow.json + +name: ci + +on: + push: + branches: + - main + pull_request: + +permissions: + contents: read + id-token: write + pull-requests: write + +jobs: + ci: + name: ${{ matrix.name }} + runs-on: ubuntu-24.04 + concurrency: + group: ${{ github.workflow }}-${{ matrix.name }}-${{ github.event.pull_request.number }} + cancel-in-progress: true + strategy: + fail-fast: false + matrix: + include: + - name: build + task: build + - name: check - format + task: check:format + install: true + annotations_ruff: true + - name: check - lint + task: check:lint + install: true + annotations_ruff: true + - name: check - types + task: check:types + install: true + annotations_mypy: true + - name: test - 3.10 + task: test + install: true + run_on_push: true + python: "3.10" + - name: test - 3.11 + task: test + install: true + run_on_push: true + python: "3.11" + - name: test - 3.12 + task: test + install: true + run_on_push: true + python: "3.12" + - name: test - 3.13 + task: test + install: true + run_on_push: true + python: "3.13" + - name: test - 3.14 + task: test + install: true + run_on_push: true + python: "3.14" + - name: pre-commit + task: install + install: true + steps: + - name: ${{ matrix.name }} + if: github.event_name == 'pull_request' || matrix.run_on_push + uses: juftin/actions/taskfile@v1 + with: + task: ${{ matrix.task }} + checkout: true + install: ${{ matrix.install || false }} + fetch-depth: ${{ matrix.name == 'pre-commit' && '0' || '1' }} + setup-uv: true + buildx: ${{ matrix.name == 'build' }} + github-token: ${{ secrets.GITHUB_TOKEN }} + annotations-ruff: ${{ matrix.annotations_ruff || false }} + annotations-mypy: ${{ matrix.annotations_mypy || false }} + annotations-pytest: ${{ matrix.python == '3.14' }} + pytest-coverage: ${{ matrix.python == '3.14' && github.event_name == 'pull_request' && !github.event.pull_request.head.repo.fork }} + env: + UV_PYTHON: ${{ matrix.python || '' }} + - name: pre-commit + if: matrix.name == 'pre-commit' && github.event_name == 'pull_request' + run: | + uv tool run --isolated \ + --with pre-commit-uv \ + pre-commit run \ + --color always \ + --from-ref ${{ github.event.pull_request.base.sha }} \ + --to-ref ${{ github.event.pull_request.head.sha }} diff --git a/tests/example-project/.github/workflows/docker.yaml b/tests/example-project/.github/workflows/docker.yaml deleted file mode 100644 index a01e6cc..0000000 --- a/tests/example-project/.github/workflows/docker.yaml +++ /dev/null @@ -1,25 +0,0 @@ -# yaml-language-server: $schema=https://www.schemastore.org/github-workflow.json - -name: docker - -on: - pull_request: - paths: - - src/example_project/** - - pyproject.toml - - .github/workflows/docker.yaml - - Dockerfile - - uv.lock - -jobs: - docker: - runs-on: ubuntu-latest - steps: - - name: docker - uses: juftin/actions/taskfile@v1 - with: - checkout: true - setup-uv: true - task: docker - buildx: true - github-token: ${{ secrets.GITHUB_TOKEN }} diff --git a/tests/example-project/.github/workflows/lint.yaml b/tests/example-project/.github/workflows/lint.yaml deleted file mode 100644 index 7f83c59..0000000 --- a/tests/example-project/.github/workflows/lint.yaml +++ /dev/null @@ -1,37 +0,0 @@ -# yaml-language-server: $schema=https://www.schemastore.org/github-workflow.json - -name: lint - -on: - pull_request: - -jobs: - lint: - runs-on: ubuntu-latest - steps: - - name: lint - id: lint - uses: juftin/actions/taskfile@v1 - continue-on-error: true - with: - checkout: true - setup-uv: true - task: lint - install: true - github-token: ${{ secrets.GITHUB_TOKEN }} - annotations-ruff: true - - name: check - id: check - uses: juftin/actions/taskfile@v1 - continue-on-error: true - with: - task: check - setup-task: false - annotations-mypy: true - - name: Raise Errors For Failures - if: | - steps.lint.outcome != 'success' || steps.check.outcome != 'success' - run: | - echo "Lint: ${{ steps.lint.outcome }}" - echo "Check: ${{ steps.check.outcome }}" - exit 1 diff --git a/tests/example-project/.github/workflows/test.yaml b/tests/example-project/.github/workflows/test.yaml deleted file mode 100644 index b02ca75..0000000 --- a/tests/example-project/.github/workflows/test.yaml +++ /dev/null @@ -1,51 +0,0 @@ -# yaml-language-server: $schema=https://www.schemastore.org/github-workflow.json - -name: test - -on: - push: - branches: - - main - paths: - - src/example_project/** - - pyproject.toml - - uv.lock - - .github/workflows/tests.yaml - pull_request: - paths: - - src/example_project/** - - pyproject.toml - - uv.lock - - .github/workflows/tests.yaml - schedule: - - cron: 0 12 1 * * - -jobs: - test: - runs-on: ubuntu-latest - strategy: - fail-fast: true - matrix: - include: - - { name: Python 3.13, python: "3.13" } - - { name: Python 3.12, python: "3.12" } - - { name: Python 3.11, python: "3.11" } - - { name: Python 3.10, python: "3.10" } - - { name: Python 3.9, python: "3.9" } - permissions: - contents: read - id-token: write - pull-requests: write - steps: - - name: test - uses: juftin/actions/taskfile@v1 - with: - checkout: true - setup-uv: true - task: test - install: true - github-token: ${{ secrets.GITHUB_TOKEN }} - annotations-pytest: ${{ matrix.python == '3.14' && true || false }} - pytest-coverage: ${{ matrix.python == '3.14' && github.event_name == 'pull_request' && true || false }} - env: - UV_PYTHON: ${{ matrix.python }} diff --git a/tests/example-project/Taskfile.yaml b/tests/example-project/Taskfile.yaml index 0f139da..3094049 100644 --- a/tests/example-project/Taskfile.yaml +++ b/tests/example-project/Taskfile.yaml @@ -5,23 +5,28 @@ version: "3" env: UV_LOCKED: 1 vars: - SOURCE_CODE: src/example_project DOCKER_REPO: juftin/example-project tasks: - ####################################### + ######################## + # DEFAULT TASK + ######################## default: - desc: Install Project, List Tasks + desc: List Available Tasks cmds: - - task --list-all - deps: [sync] - ####################################### + - task --list-all --sort alphanumeric + silent: true + ######################## + # INSTALL AND DEPENDENCIES + ######################## install: desc: Install Project + Dev Dependencies cmds: - task: pre-commit-install - task: sync - ####################################### + ######################## + # TESTING + ######################## test: desc: Run Tests cmds: @@ -35,52 +40,72 @@ tasks: UV_PYTHON: "{{.UV_PYTHON}}" ####################################### test:matrix: - desc: Run Tests with different Python versions + desc: Run Tests across Python Versions cmds: - for: matrix: - PYTHON: ["3.9", "3.10", "3.11", "3.12", "3.13"] + PYTHON: ["3.10", "3.11", "3.12", "3.13", "3.14"] task: test vars: UV_PYTHON: "{{.ITEM.PYTHON}}" - ####################################### - lint: - desc: Run Code Linters + ######################## + # CODE QUALITY CHECKS + ######################## + check:format: + desc: Code Quality Check - Formatting cmds: - - uv run -- ruff check {{.CLI_ARGS | default .ROOT_DIR}} - task: fmt vars: CLI_ARGS: --check - deps: [sync] ####################################### - fmt: - desc: Run Code Formatters + check:lint: + desc: Code Quality Check - Linting cmds: - - uv run -- ruff format {{.CLI_ARGS | default .ROOT_DIR}} + - uv run -- ruff check {{.CLI_ARGS | default .ROOT_DIR}} deps: [sync] ####################################### - fix: - desc: Fix Code with Linters and Formatters + check:style: + desc: Code Quality Check - Formatting + Linting cmds: - - task: fmt - - uv run -- ruff check --fix {{.CLI_ARGS | default .ROOT_DIR}} - deps: [sync] + - task: check:format + - task: check:lint ####################################### - check: - desc: Run Static Type Checkers + check:types: + desc: Code Quality Check - Type Checking cmds: - | uv run -- \ mypy \ --ignore-missing-imports \ - {{.CLI_ARGS | default .SOURCE_CODE}} + {{.CLI_ARGS | default (printf "%s/src" .ROOT_DIR)}} status: - '[$(uv tree --package mypy) != ""]' - requires: - vars: [SOURCE_CODE] ####################################### + check: + desc: Code Quality Check - Formatting + Linting + Type Checking + cmds: + - task: check:style + - task: check:types + ######################## + # CODE QUALITY FIXES + ######################## + fmt: + desc: Code Quality Auto-Fix - Formatting + cmds: + - uv run -- ruff format {{.CLI_ARGS | default .ROOT_DIR}} + deps: [sync] + ####################################### + fix: + desc: Code Quality Auto-Fix - Formatting + Linting + cmds: + - task: fmt + - uv run -- ruff check --fix {{.CLI_ARGS | default .ROOT_DIR}} + deps: [sync] + ######################## + # BUILD AND PUBLISH + ######################## dist: - desc: Build the Project Artifacts + desc: Build Distribution Artifacts cmds: - cmd: rm -rf {{.ROOT_DIR}}/dist - cmd: uv build @@ -103,13 +128,13 @@ tasks: - test ! -f {{.ROOT_DIR}}/{{.DOCKERFILE | default "Dockerfile"}} ####################################### build: - desc: Build the Project Artifacts + desc: Build Project Artifacts cmds: - task: dist - task: docker ####################################### publish: - desc: Publish the Project Artifacts + desc: Publish Docker Image cmds: - task: docker vars: @@ -118,20 +143,22 @@ tasks: vars: [CI, RELEASE_VERSION] status: - test ! -f {{.ROOT_DIR}}/{{.DOCKERFILE | default "Dockerfile"}} - ####################################### + ######################## + # DOCUMENTATION AND COMMANDS + ######################## docs: - desc: Generate Documentation + desc: Run Documentation Site cmds: - uv run --group docs -- mkdocs {{.CLI_ARGS | default "serve"}} ####################################### run: - desc: Run Command within Project (requires "--") + desc: Run Project Command (requires "--") interactive: true cmds: - uv run -- {{.CLI_ARGS}} ####################################### docker-run: - desc: Run Command within Docker Container (supports "--" for command) + desc: Run Docker Command (requires "--") interactive: true cmds: - | @@ -147,16 +174,18 @@ tasks: - test ! -f {{.ROOT_DIR}}/{{.DOCKERFILE | default "Dockerfile"}} requires: vars: [DOCKER_REPO] - ####################################### + ######################## + # DEPENDENCY MANAGEMENT + ######################## lock: - desc: Regenerate the Project Lockfile + desc: Regenerate Project Lockfile cmds: - uv lock env: UV_LOCKED: "0" - ####################################### + ######################## # INTERNAL TASKS - ####################################### + ######################## sync: desc: Install Project Dependencies internal: true diff --git a/tests/example-project/docs/contributing.md b/tests/example-project/docs/contributing.md index 03b064c..7a74eed 100644 --- a/tests/example-project/docs/contributing.md +++ b/tests/example-project/docs/contributing.md @@ -18,7 +18,7 @@ manager) and **[task]** (task runner). Once both are installed, you can use sh -c "$(curl --location https://taskfile.dev/install.sh)" -- -d ``` -3. Install project dependencies and list available tasks +3. Install the project development environment ```shell task install @@ -34,18 +34,27 @@ manager) and **[task]** (task runner). Once both are installed, you can use ### Task Cheat Sheet -| Command Description | Command | Notes | -| ------------------- | ------------------- | --------------------------------------- | -| Install Project | `task install` | Installs project and dev dependencies | -| Run Tests | `task test` | Runs tests with `pytest` | -| Run Linting | `task lint` | Lints code with `ruff` | -| Fix Code Issues | `task fix` | Formats and auto-fixes code with `ruff` | -| Run Formatting | `task fmt` | Formats code with `ruff` | -| Run Type Checking | `task check` | Runs static analysis with `mypy` | -| Build Project | `task build` | Builds project artifacts | -| Update Dependencies | `task lock` | Regenerates project lockfile | -| Serve Documentation | `task docs` | Serves docs with `mkdocs` | -| Run Commands | `task run -- ` | Runs arbitrary commands | +| Command Description | Command | Notes | +| ------------------------------- | --------------------------- | ----------------------------------------------- | +| List Available Tasks | `task` | Lists tasks in alphanumeric order | +| Install Project + Dev Dependencies | `task install` | Installs dependencies and pre-commit hooks | +| Run Tests | `task test` | Runs tests with `pytest` | +| Run Tests across Python Versions | `task test:matrix` | Runs tests on Python 3.10 through 3.14 | +| Code Quality Check - Formatting | `task check:format` | Checks formatting with `ruff` | +| Code Quality Check - Linting | `task check:lint` | Checks linting with `ruff` | +| Code Quality Check - Formatting + Linting | `task check:style` | Runs formatting and linting checks | +| Code Quality Check - Type Checking | `task check:types` | Checks types with `mypy` | +| Code Quality Check - Formatting + Linting + Type Checking | `task check` | Runs all code checks | +| Code Quality Auto-Fix - Formatting | `task fmt` | Formats code with `ruff` | +| Code Quality Auto-Fix - Formatting + Linting | `task fix` | Formats code and applies `ruff` fixes | +| Build Distribution Artifacts | `task dist` | Builds Python distribution artifacts | +| Build Docker Image | `task docker` | Builds the Docker image when a Dockerfile exists | +| Build Project Artifacts | `task build` | Builds distribution artifacts and Docker image | +| Publish Docker Image | `task publish` | Pushes the Docker image in CI | +| Run Documentation Site | `task docs` | Runs MkDocs | +| Run Project Command | `task run -- ` | Runs a command in the project environment | +| Run Docker Command | `task docker-run -- ` | Runs a command in the Docker image | +| Regenerate Project Lockfile | `task lock` | Regenerates `uv.lock` | ### Task Explanation diff --git a/tests/test_copier.py b/tests/test_copier.py index e494a91..5062dfd 100644 --- a/tests/test_copier.py +++ b/tests/test_copier.py @@ -2,6 +2,7 @@ import shutil import subprocess +import sys from pathlib import Path import pytest @@ -280,3 +281,77 @@ def test_copier_updates_using_configured_answers_file(tmp_path: Path) -> None: ) assert marker in (project / "README.md").read_text() + + +def test_copier_removes_consolidated_ci_workflows(tmp_path: Path) -> None: + """Delete replaced CI workflows, including downstream customizations.""" + template_root = Path(__file__).parent.parent + source = tmp_path / "template-source" + shutil.copytree( + template_root, + source, + ignore=shutil.ignore_patterns( + ".git", + ".venv", + ".pytest_cache", + "__pycache__", + "site", + ), + ) + workflow_dir = source / "template/.github/workflows" + (workflow_dir / "ci.yaml").unlink() + legacy_workflows = { + "test.yaml": "test.yaml", + "lint.yaml": "lint.yaml", + "docker.yaml": "{% if build_docker_image %}docker.yaml{% endif %}", + } + for template_path in legacy_workflows.values(): + (workflow_dir / template_path).write_text("name: legacy\n") + + _initialize_repository(source) + _commit_all(source, "Initial template") + _git(source, "tag", "v1.0.0") + + project = tmp_path / "generated-project" + run_copy( + src_path=str(source), + dst_path=project, + data={"build_docker_image": True}, + defaults=True, + overwrite=True, + quiet=True, + vcs_ref="v1.0.0", + ) + _initialize_repository(project) + for filename in legacy_workflows: + workflow = project / ".github/workflows" / filename + workflow.write_text(f"{workflow.read_text()}# Downstream customization\n") + _commit_all(project, "Initial project") + + (workflow_dir / "ci.yaml").write_text( + (template_root / "template/.github/workflows/ci.yaml").read_text() + ) + for template_path in legacy_workflows.values(): + (workflow_dir / template_path).unlink() + _commit_all(source, "Consolidate CI workflows") + _git(source, "tag", "v1.1.0") + + subprocess.run( + [ + sys.executable, + "-m", + "copier", + "update", + "--defaults", + "--answers-file", + ".github/.copier-answers.yaml", + str(project), + ], + check=True, + capture_output=True, + text=True, + ) + + assert (project / ".github/workflows/ci.yaml").is_file() + for filename in legacy_workflows: + assert not (project / ".github/workflows" / filename).exists()