├── src └── ss_python │ ├── py.typed │ ├── __init__.py │ ├── cli.py │ └── settings.py ├── template ├── src │ └── {{ module_name }} │ │ ├── py.typed │ │ ├── __init__.py │ │ ├── cli.py.jinja │ │ └── settings.py.jinja ├── tests │ ├── __init__.py │ ├── pkg_test.py.jinja │ ├── settings_test.py.jinja │ └── cli_test.py.jinja ├── {{_copier_conf.answers_file}}.jinja ├── docs │ ├── api │ │ ├── index.md.jinja │ │ └── settings.md.jinja │ ├── reports │ │ ├── mypy │ │ │ └── index.md │ │ ├── coverage │ │ │ └── index.md │ │ └── index.md │ ├── cli │ │ ├── index.md.jinja │ │ ├── run.md.jinja │ │ └── {{ module_name }}.md.jinja │ ├── _static │ │ ├── images │ │ │ ├── dev-container-reopen-prompt.png │ │ │ ├── bootstrap-dev-container-github.png │ │ │ └── bootstrap-dev-container-gitlab.png │ │ └── badges │ │ │ └── {% if project_name == 'Serious Scaffold Python' %}logo.json{% endif %}.jinja │ ├── advanced │ │ ├── index.md │ │ ├── partial-dev-env.md │ │ ├── cicd.md.jinja │ │ └── dev-containers.md.jinja │ ├── management │ │ ├── index.md │ │ ├── init.md.jinja │ │ ├── update.md │ │ └── release.md.jinja │ ├── development │ │ ├── index.md │ │ ├── cleanup-dev-env.md │ │ ├── setup-dev-env.md │ │ ├── tests.md.jinja │ │ ├── commit.md │ │ └── git-workflow.md.jinja │ ├── index.md.jinja │ └── conf.py.jinja ├── {% if repo_platform == 'github' %}.github{% endif %} │ ├── FUNDING.yml.jinja │ └── workflows │ │ ├── readthedocs-preview.yml.jinja │ │ ├── commitlint.yml │ │ ├── delete-untagged-packages.yml.jinja │ │ ├── semantic-release.yml.jinja │ │ ├── ci.yml.jinja │ │ ├── renovate.yml.jinja │ │ ├── devcontainer.yml.jinja │ │ └── release.yml.jinja ├── .devcontainer │ ├── Dockerfile.dockerignore │ ├── devcontainer.json.jinja │ └── Dockerfile.jinja ├── LICENSE.jinja ├── .vscode │ ├── extensions.json │ └── settings.json ├── {% if repo_platform == 'gitlab' or repo_platform == 'gitlab-self-managed' %}.gitlab-ci.yml{% endif %}.jinja ├── scripts │ └── generate-coverage-badge.sh ├── {% if repo_platform == 'gitlab' or repo_platform == 'gitlab-self-managed' %}.gitlab{% endif %} │ └── workflows │ │ ├── semantic-release.yml.jinja │ │ ├── commitlint.yml │ │ ├── renovate.yml │ │ ├── ci.yml.jinja │ │ ├── devcontainer.yml.jinja │ │ └── release.yml.jinja ├── .readthedocs.yaml.jinja ├── README.md.jinja ├── .pre-commit-config.yaml.jinja ├── .gitignore.jinja ├── .releaserc.json.jinja └── pyproject.toml.jinja ├── .github ├── FUNDING.yml └── workflows │ ├── readthedocs-preview.yml │ ├── commitlint.yml │ ├── delete-untagged-packages.yml │ ├── ci.yml │ ├── semantic-release.yml │ ├── renovate.yml │ ├── devcontainer.yml │ └── release.yml ├── tests ├── __init__.py ├── pkg_test.py ├── settings_test.py └── cli_test.py ├── docs ├── api │ ├── index.md │ └── settings.md ├── reports │ ├── mypy │ │ └── index.md │ ├── coverage │ │ └── index.md │ └── index.md ├── cli │ ├── index.md │ ├── ss_python.md │ └── run.md ├── _static │ ├── images │ │ ├── dev-container-reopen-prompt.png │ │ ├── bootstrap-dev-container-github.png │ │ └── bootstrap-dev-container-gitlab.png │ └── badges │ │ └── logo.json ├── advanced │ ├── index.md │ ├── partial-dev-env.md │ ├── cicd.md │ └── dev-containers.md ├── management │ ├── index.md │ ├── init.md │ ├── update.md │ └── release.md ├── development │ ├── index.md │ ├── cleanup-dev-env.md │ ├── setup-dev-env.md │ ├── tests.md │ ├── commit.md │ └── git-workflow.md ├── index.md └── conf.py ├── .devcontainer ├── Dockerfile.dockerignore ├── devcontainer.json └── Dockerfile ├── .gitlab-ci.yml ├── .vscode ├── extensions.json └── settings.json ├── .gitlab └── workflows │ ├── semantic-release.yml │ ├── commitlint.yml │ ├── renovate.yml │ ├── ci.yml │ ├── devcontainer.yml │ └── release.yml ├── scripts └── generate-coverage-badge.sh ├── includes ├── copier-answers-sample.yml ├── version_compare.jinja ├── licenses │ ├── MIT License.jinja │ ├── The Unlicense (Unlicense).jinja │ └── Boost Software License 1.0 (BSL-1.0).jinja ├── variable.jinja └── sample.jinja ├── LICENSE ├── .readthedocs.yaml ├── pyproject.toml ├── .pre-commit-config.yaml ├── .gitignore └── .releaserc.json /src/ss_python/py.typed: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /template/src/{{ module_name }}/py.typed: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: 2 | - huxuan 3 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- 1 | """Init for the test.""" 2 | -------------------------------------------------------------------------------- /src/ss_python/__init__.py: -------------------------------------------------------------------------------- 1 | """Init for the project.""" 2 | -------------------------------------------------------------------------------- /template/tests/__init__.py: -------------------------------------------------------------------------------- 1 | """Init for the test.""" 2 | -------------------------------------------------------------------------------- /template/src/{{ module_name }}/__init__.py: -------------------------------------------------------------------------------- 1 | """Init for the project.""" 2 | -------------------------------------------------------------------------------- /template/{{_copier_conf.answers_file}}.jinja: -------------------------------------------------------------------------------- 1 | {{ _copier_answers|to_nice_yaml -}} 2 | -------------------------------------------------------------------------------- /docs/api/index.md: -------------------------------------------------------------------------------- 1 | # API Reference 2 | 3 | ```{toctree} 4 | :maxdepth: 1 5 | settings 6 | ``` 7 | -------------------------------------------------------------------------------- /docs/reports/mypy/index.md: -------------------------------------------------------------------------------- 1 | # MyPy Reports 2 | 3 | 4 | -------------------------------------------------------------------------------- /docs/cli/index.md: -------------------------------------------------------------------------------- 1 | # CLI Reference 2 | 3 | ```{toctree} 4 | :maxdepth: 1 5 | ss_python 6 | run 7 | ``` 8 | -------------------------------------------------------------------------------- /template/docs/api/index.md.jinja: -------------------------------------------------------------------------------- 1 | # API Reference 2 | 3 | ```{toctree} 4 | :maxdepth: 1 5 | settings 6 | ``` 7 | -------------------------------------------------------------------------------- /template/docs/reports/mypy/index.md: -------------------------------------------------------------------------------- 1 | # MyPy Reports 2 | 3 | 4 | -------------------------------------------------------------------------------- /docs/api/settings.md: -------------------------------------------------------------------------------- 1 | # ss_python.settings 2 | 3 | ```{eval-rst} 4 | .. automodule:: ss_python.settings 5 | ``` 6 | -------------------------------------------------------------------------------- /docs/reports/coverage/index.md: -------------------------------------------------------------------------------- 1 | # Coverage Reports 2 | 3 | 4 | -------------------------------------------------------------------------------- /template/{% if repo_platform == 'github' %}.github{% endif %}/FUNDING.yml.jinja: -------------------------------------------------------------------------------- 1 | github: 2 | - {{ author_name }} 3 | -------------------------------------------------------------------------------- /template/docs/reports/coverage/index.md: -------------------------------------------------------------------------------- 1 | # Coverage Reports 2 | 3 | 4 | -------------------------------------------------------------------------------- /.devcontainer/Dockerfile.dockerignore: -------------------------------------------------------------------------------- 1 | * 2 | .* 3 | !/Makefile 4 | !/README.md 5 | !/pdm.lock 6 | !/pyproject.toml 7 | !/src/ 8 | -------------------------------------------------------------------------------- /docs/reports/index.md: -------------------------------------------------------------------------------- 1 | # Code Quality Reports 2 | 3 | ```{toctree} 4 | :maxdepth: 2 5 | mypy/index 6 | coverage/index 7 | ``` 8 | -------------------------------------------------------------------------------- /template/docs/cli/index.md.jinja: -------------------------------------------------------------------------------- 1 | # CLI Reference 2 | 3 | ```{toctree} 4 | :maxdepth: 1 5 | {{ module_name }} 6 | run 7 | ``` 8 | -------------------------------------------------------------------------------- /template/.devcontainer/Dockerfile.dockerignore: -------------------------------------------------------------------------------- 1 | * 2 | .* 3 | !/Makefile 4 | !/README.md 5 | !/pdm.lock 6 | !/pyproject.toml 7 | !/src/ 8 | -------------------------------------------------------------------------------- /template/docs/reports/index.md: -------------------------------------------------------------------------------- 1 | # Code Quality Reports 2 | 3 | ```{toctree} 4 | :maxdepth: 2 5 | mypy/index 6 | coverage/index 7 | ``` 8 | -------------------------------------------------------------------------------- /template/LICENSE.jinja: -------------------------------------------------------------------------------- 1 | {% set license_template = 'includes/licenses/' + copyright_license + '.jinja' %} 2 | {% include license_template %} 3 | -------------------------------------------------------------------------------- /template/docs/api/settings.md.jinja: -------------------------------------------------------------------------------- 1 | # {{ module_name }}.settings 2 | 3 | ```{eval-rst} 4 | .. automodule:: {{ module_name }}.settings 5 | ``` 6 | -------------------------------------------------------------------------------- /docs/cli/ss_python.md: -------------------------------------------------------------------------------- 1 | # Serious Scaffold Python 2 | 3 | ```{eval-rst} 4 | .. click:: ss_python.cli:cli 5 | :prog: ss-python-cli 6 | :nested: short 7 | ``` 8 | -------------------------------------------------------------------------------- /docs/_static/images/dev-container-reopen-prompt.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/serious-scaffold/ss-python/HEAD/docs/_static/images/dev-container-reopen-prompt.png -------------------------------------------------------------------------------- /docs/cli/run.md: -------------------------------------------------------------------------------- 1 | # Serious Scaffold Python Run 2 | 3 | ```{eval-rst} 4 | .. click:: ss_python.cli:run 5 | :prog: ss-python-cli run 6 | :nested: full 7 | ``` 8 | -------------------------------------------------------------------------------- /docs/_static/images/bootstrap-dev-container-github.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/serious-scaffold/ss-python/HEAD/docs/_static/images/bootstrap-dev-container-github.png -------------------------------------------------------------------------------- /docs/_static/images/bootstrap-dev-container-gitlab.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/serious-scaffold/ss-python/HEAD/docs/_static/images/bootstrap-dev-container-gitlab.png -------------------------------------------------------------------------------- /template/docs/_static/images/dev-container-reopen-prompt.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/serious-scaffold/ss-python/HEAD/template/docs/_static/images/dev-container-reopen-prompt.png -------------------------------------------------------------------------------- /docs/_static/badges/logo.json: -------------------------------------------------------------------------------- 1 | { 2 | "color": "#7FDBFF", 3 | "label": "Serious Scaffold", 4 | "labelColor": "#0074D9", 5 | "message": "Python", 6 | "schemaVersion": 1 7 | } 8 | -------------------------------------------------------------------------------- /template/docs/_static/images/bootstrap-dev-container-github.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/serious-scaffold/ss-python/HEAD/template/docs/_static/images/bootstrap-dev-container-github.png -------------------------------------------------------------------------------- /template/docs/_static/images/bootstrap-dev-container-gitlab.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/serious-scaffold/ss-python/HEAD/template/docs/_static/images/bootstrap-dev-container-gitlab.png -------------------------------------------------------------------------------- /tests/pkg_test.py: -------------------------------------------------------------------------------- 1 | """Test for pkg.""" 2 | 3 | import ss_python 4 | 5 | 6 | def test_pkg() -> None: 7 | """Test for pkg.""" 8 | assert ss_python.__package__ == "ss_python" 9 | -------------------------------------------------------------------------------- /template/docs/cli/run.md.jinja: -------------------------------------------------------------------------------- 1 | # {{ project_name }} Run 2 | 3 | ```{eval-rst} 4 | .. click:: {{ module_name }}.cli:run 5 | :prog: {{ package_name }}-cli run 6 | :nested: full 7 | ``` 8 | -------------------------------------------------------------------------------- /template/docs/cli/{{ module_name }}.md.jinja: -------------------------------------------------------------------------------- 1 | # {{ project_name }} 2 | 3 | ```{eval-rst} 4 | .. click:: {{ module_name }}.cli:cli 5 | :prog: {{ package_name }}-cli 6 | :nested: short 7 | ``` 8 | -------------------------------------------------------------------------------- /template/tests/pkg_test.py.jinja: -------------------------------------------------------------------------------- 1 | """Test for pkg.""" 2 | 3 | import {{ module_name }} 4 | 5 | 6 | def test_pkg() -> None: 7 | """Test for pkg.""" 8 | assert {{ module_name }}.__package__ == "{{ module_name }}" 9 | -------------------------------------------------------------------------------- /template/docs/_static/badges/{% if project_name == 'Serious Scaffold Python' %}logo.json{% endif %}.jinja: -------------------------------------------------------------------------------- 1 | { 2 | "color": "#7FDBFF", 3 | "label": "Serious Scaffold", 4 | "labelColor": "#0074D9", 5 | "message": "Python", 6 | "schemaVersion": 1 7 | } 8 | -------------------------------------------------------------------------------- /.gitlab-ci.yml: -------------------------------------------------------------------------------- 1 | stages: 2 | - build 3 | - ci 4 | - release 5 | default: 6 | before_script: 7 | - env | sort 8 | image: ${CI_REGISTRY_IMAGE}/dev:py3.12 9 | retry: 10 | max: 2 11 | when: runner_system_failure 12 | include: 13 | - local: .gitlab/workflows/**.yml 14 | -------------------------------------------------------------------------------- /src/ss_python/cli.py: -------------------------------------------------------------------------------- 1 | """Command Line Interface.""" 2 | 3 | import click 4 | 5 | 6 | @click.group() 7 | @click.version_option() 8 | def cli() -> None: 9 | """CLI for Serious Scaffold Python.""" 10 | 11 | 12 | @cli.command() 13 | def run() -> None: 14 | """Run command.""" 15 | -------------------------------------------------------------------------------- /template/src/{{ module_name }}/cli.py.jinja: -------------------------------------------------------------------------------- 1 | """Command Line Interface.""" 2 | 3 | import click 4 | 5 | 6 | @click.group() 7 | @click.version_option() 8 | def cli() -> None: 9 | """CLI for {{ project_name }}.""" 10 | 11 | 12 | @cli.command() 13 | def run() -> None: 14 | """Run command.""" 15 | -------------------------------------------------------------------------------- /docs/advanced/index.md: -------------------------------------------------------------------------------- 1 | # Advanced Usage 2 | 3 | This section provides recommended best practices for enhancing your development workflow. While not essential, these topics can optimize the project management and development processes. 4 | 5 | ```{toctree} 6 | dev-containers 7 | partial-dev-env 8 | cicd 9 | ``` 10 | -------------------------------------------------------------------------------- /template/docs/advanced/index.md: -------------------------------------------------------------------------------- 1 | # Advanced Usage 2 | 3 | This section provides recommended best practices for enhancing your development workflow. While not essential, these topics can optimize the project management and development processes. 4 | 5 | ```{toctree} 6 | dev-containers 7 | partial-dev-env 8 | cicd 9 | ``` 10 | -------------------------------------------------------------------------------- /docs/management/index.md: -------------------------------------------------------------------------------- 1 | # Project Management 2 | 3 | This section is designed for project maintainers and covers essential tasks for managing your project. Follow these guidelines to ensure your project remains up-to-date and adheres to best practices. 4 | 5 | ```{toctree} 6 | init 7 | settings 8 | update 9 | release 10 | ``` 11 | -------------------------------------------------------------------------------- /template/docs/management/index.md: -------------------------------------------------------------------------------- 1 | # Project Management 2 | 3 | This section is designed for project maintainers and covers essential tasks for managing your project. Follow these guidelines to ensure your project remains up-to-date and adheres to best practices. 4 | 5 | ```{toctree} 6 | init 7 | settings 8 | update 9 | release 10 | ``` 11 | -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": [ 3 | "DavidAnson.vscode-markdownlint", 4 | "ExecutableBookProject.myst-highlight", 5 | "charliermarsh.ruff", 6 | "ms-python.mypy-type-checker", 7 | "ms-python.python", 8 | "ms-vscode-remote.remote-containers", 9 | "richie5um2.vscode-sort-json", 10 | "streetsidesoftware.code-spell-checker" 11 | ] 12 | } 13 | -------------------------------------------------------------------------------- /docs/development/index.md: -------------------------------------------------------------------------------- 1 | # Development Practices 2 | 3 | This section is designed for developers and covers essential topics during daily development lifecycle. Follow these guidelines to ensure all contributors adhere to best practices, maintain code quality, and collaborate efficiently. 4 | 5 | ```{toctree} 6 | git-workflow 7 | setup-dev-env 8 | cleanup-dev-env 9 | commit 10 | tests 11 | ``` 12 | -------------------------------------------------------------------------------- /template/.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": [ 3 | "DavidAnson.vscode-markdownlint", 4 | "ExecutableBookProject.myst-highlight", 5 | "charliermarsh.ruff", 6 | "ms-python.mypy-type-checker", 7 | "ms-python.python", 8 | "ms-vscode-remote.remote-containers", 9 | "richie5um2.vscode-sort-json", 10 | "streetsidesoftware.code-spell-checker" 11 | ] 12 | } 13 | -------------------------------------------------------------------------------- /template/{% if repo_platform == 'gitlab' or repo_platform == 'gitlab-self-managed' %}.gitlab-ci.yml{% endif %}.jinja: -------------------------------------------------------------------------------- 1 | stages: 2 | - build 3 | - ci 4 | - release 5 | default: 6 | before_script: 7 | - env | sort 8 | image: ${CI_REGISTRY_IMAGE}/dev:py{{ default_py }} 9 | retry: 10 | max: 2 11 | when: runner_system_failure 12 | include: 13 | - local: .gitlab/workflows/**.yml 14 | -------------------------------------------------------------------------------- /template/docs/development/index.md: -------------------------------------------------------------------------------- 1 | # Development Practices 2 | 3 | This section is designed for developers and covers essential topics during daily development lifecycle. Follow these guidelines to ensure all contributors adhere to best practices, maintain code quality, and collaborate efficiently. 4 | 5 | ```{toctree} 6 | git-workflow 7 | setup-dev-env 8 | cleanup-dev-env 9 | commit 10 | tests 11 | ``` 12 | -------------------------------------------------------------------------------- /tests/settings_test.py: -------------------------------------------------------------------------------- 1 | """Test for settings.""" 2 | 3 | import os 4 | 5 | from ss_python.settings import global_settings, settings 6 | 7 | 8 | def test_settings() -> None: 9 | """Test for settings.""" 10 | assert settings.logging_level == os.getenv( 11 | "SS_PYTHON_LOGGING_LEVEL", 12 | "INFO", 13 | ) 14 | assert str(global_settings.ci).lower() == os.getenv("CI", "False").lower() 15 | -------------------------------------------------------------------------------- /template/tests/settings_test.py.jinja: -------------------------------------------------------------------------------- 1 | """Test for settings.""" 2 | 3 | import os 4 | 5 | from {{ module_name }}.settings import global_settings, settings 6 | 7 | 8 | def test_settings() -> None: 9 | """Test for settings.""" 10 | assert settings.logging_level == os.getenv( 11 | "{{ module_name|upper }}_LOGGING_LEVEL", 12 | "INFO", 13 | ) 14 | assert str(global_settings.ci).lower() == os.getenv("CI", "False").lower() 15 | -------------------------------------------------------------------------------- /docs/index.md: -------------------------------------------------------------------------------- 1 | # Welcome to Serious Scaffold Python's documentation 2 | 3 | ```{toctree} 4 | :hidden: 5 | Overview 6 | management/index 7 | development/index 8 | advanced/index 9 | cli/index 10 | api/index 11 | reports/index 12 | Changelog 13 | ``` 14 | 15 | ```{include} ../README.md 16 | :start-line: 1 17 | ``` 18 | 19 | ## 🔖 Indices and tables 20 | 21 | * {ref}`genindex` 22 | * {ref}`modindex` 23 | * {ref}`search` 24 | -------------------------------------------------------------------------------- /template/docs/index.md.jinja: -------------------------------------------------------------------------------- 1 | {% from pathjoin("includes", "variable.jinja") import releases_url with context %} 2 | # Welcome to {{ project_name }}'s documentation 3 | 4 | ```{toctree} 5 | :hidden: 6 | Overview 7 | management/index 8 | development/index 9 | advanced/index 10 | cli/index 11 | api/index 12 | reports/index 13 | Changelog <{{ releases_url() }}> 14 | ``` 15 | 16 | ```{include} ../README.md 17 | :start-line: 1 18 | ``` 19 | 20 | ## 🔖 Indices and tables 21 | 22 | * {ref}`genindex` 23 | * {ref}`modindex` 24 | * {ref}`search` 25 | -------------------------------------------------------------------------------- /tests/cli_test.py: -------------------------------------------------------------------------------- 1 | """Test for cli.""" 2 | 3 | from click.testing import CliRunner 4 | 5 | from ss_python.cli import cli 6 | 7 | 8 | def test_cli() -> None: 9 | """Test for cli.""" 10 | runner = CliRunner() 11 | result = runner.invoke(cli) 12 | assert result.exit_code == 0 13 | assert "Usage" in result.output 14 | 15 | 16 | def test_cli_run() -> None: 17 | """Test for run subcommand of the cli.""" 18 | runner = CliRunner() 19 | result = runner.invoke(cli, "run") 20 | assert result.exit_code == 0 21 | assert not result.output 22 | -------------------------------------------------------------------------------- /template/tests/cli_test.py.jinja: -------------------------------------------------------------------------------- 1 | """Test for cli.""" 2 | 3 | from click.testing import CliRunner 4 | 5 | from {{ module_name }}.cli import cli 6 | 7 | 8 | def test_cli() -> None: 9 | """Test for cli.""" 10 | runner = CliRunner() 11 | result = runner.invoke(cli) 12 | assert result.exit_code == 0 13 | assert "Usage" in result.output 14 | 15 | 16 | def test_cli_run() -> None: 17 | """Test for run subcommand of the cli.""" 18 | runner = CliRunner() 19 | result = runner.invoke(cli, "run") 20 | assert result.exit_code == 0 21 | assert not result.output 22 | -------------------------------------------------------------------------------- /.gitlab/workflows/semantic-release.yml: -------------------------------------------------------------------------------- 1 | semantic-release: 2 | image: 3 | name: node:24.11.1@sha256:aa648b387728c25f81ff811799bbf8de39df66d7e2d9b3ab55cc6300cb9175d9 4 | rules: 5 | - if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH && $CI_PIPELINE_SOURCE == "push" && $CI_PROJECT_NAMESPACE == "serious-scaffold" && $CI_PROJECT_NAME == "ss-python" && $PAT != null 6 | script: 7 | - > 8 | npx 9 | --package @semantic-release/gitlab@13.2.9 10 | --package conventional-changelog-conventionalcommits@9.1.0 11 | --package semantic-release@25.0.2 12 | semantic-release 13 | stage: release 14 | variables: 15 | GITLAB_TOKEN: $PAT 16 | -------------------------------------------------------------------------------- /scripts/generate-coverage-badge.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | TOTAL_COVERAGE=$(coverage report --format=total) 4 | COLOR="#9f9f9f" 5 | 6 | if [ "$TOTAL_COVERAGE" -gt 95 ]; then 7 | COLOR="#4c1" 8 | elif [ "$TOTAL_COVERAGE" -gt 90 ]; then 9 | COLOR="#a3c51c" 10 | elif [ "$TOTAL_COVERAGE" -gt 75 ]; then 11 | COLOR="#dfb317" 12 | elif [ "$TOTAL_COVERAGE" -gt 0 ]; then 13 | COLOR="#e05d44" 14 | fi 15 | 16 | COVERAGE_JSON_DIR=${1:-.} 17 | mkdir -p "$COVERAGE_JSON_DIR" 18 | 19 | cat << EOF > "${COVERAGE_JSON_DIR}/coverage.json" 20 | { 21 | "schemaVersion": 1, 22 | "label": "coverage", 23 | "message": "${TOTAL_COVERAGE}%", 24 | "color": "${COLOR}" 25 | } 26 | EOF 27 | -------------------------------------------------------------------------------- /template/scripts/generate-coverage-badge.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | TOTAL_COVERAGE=$(coverage report --format=total) 4 | COLOR="#9f9f9f" 5 | 6 | if [ "$TOTAL_COVERAGE" -gt 95 ]; then 7 | COLOR="#4c1" 8 | elif [ "$TOTAL_COVERAGE" -gt 90 ]; then 9 | COLOR="#a3c51c" 10 | elif [ "$TOTAL_COVERAGE" -gt 75 ]; then 11 | COLOR="#dfb317" 12 | elif [ "$TOTAL_COVERAGE" -gt 0 ]; then 13 | COLOR="#e05d44" 14 | fi 15 | 16 | COVERAGE_JSON_DIR=${1:-.} 17 | mkdir -p "$COVERAGE_JSON_DIR" 18 | 19 | cat << EOF > "${COVERAGE_JSON_DIR}/coverage.json" 20 | { 21 | "schemaVersion": 1, 22 | "label": "coverage", 23 | "message": "${TOTAL_COVERAGE}%", 24 | "color": "${COLOR}" 25 | } 26 | EOF 27 | -------------------------------------------------------------------------------- /includes/copier-answers-sample.yml: -------------------------------------------------------------------------------- 1 | author_email: i@huxuan.org 2 | author_name: huxuan 3 | copyright_holder: Serious Scaffold 4 | copyright_license: MIT License 5 | copyright_year: 2022-2025 6 | coverage_threshold: 100 7 | default_py: "3.12" 8 | development_status: Alpha 9 | max_py: "3.13" 10 | min_py: "3.9" 11 | module_name: ss_python 12 | organization_name: Serious Scaffold 13 | package_name: ss-python 14 | platforms: 15 | - macos 16 | - linux 17 | - windows 18 | project_description: A Python Project Template for Long-Term Maintainability. 19 | project_keywords: copier-template, project-template, long-term-maintainability 20 | project_name: Serious Scaffold Python 21 | repo_name: ss-python 22 | repo_namespace: serious-scaffold 23 | repo_platform: github 24 | -------------------------------------------------------------------------------- /template/{% if repo_platform == 'gitlab' or repo_platform == 'gitlab-self-managed' %}.gitlab{% endif %}/workflows/semantic-release.yml.jinja: -------------------------------------------------------------------------------- 1 | semantic-release: 2 | image: 3 | name: node:24.11.1@sha256:aa648b387728c25f81ff811799bbf8de39df66d7e2d9b3ab55cc6300cb9175d9 4 | rules: 5 | - if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH && $CI_PIPELINE_SOURCE == "push" && $CI_PROJECT_NAMESPACE == "{{ repo_namespace }}" && $CI_PROJECT_NAME == "{{ repo_name }}" && $PAT != null 6 | script: 7 | - > 8 | npx 9 | --package @semantic-release/gitlab@13.2.9 10 | --package conventional-changelog-conventionalcommits@9.1.0 11 | --package semantic-release@25.0.2 12 | semantic-release 13 | stage: release 14 | variables: 15 | GITLAB_TOKEN: $PAT 16 | -------------------------------------------------------------------------------- /.gitlab/workflows/commitlint.yml: -------------------------------------------------------------------------------- 1 | commitlint: 2 | image: 3 | name: commitlint/commitlint:20.1.0@sha256:caf971bc6ab2744d20f9967df648f210d6e79dda4055748d183d31700986f115 4 | entrypoint: [''] 5 | interruptible: true 6 | rules: 7 | - if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH && $CI_PIPELINE_SOURCE == "push" 8 | - if: $CI_PIPELINE_SOURCE == 'merge_request_event' 9 | script: 10 | - | 11 | if [ "$CI_PIPELINE_SOURCE" = "push" ]; then 12 | echo "$CI_COMMIT_TITLE" | commitlint -x @commitlint/config-conventional 13 | elif [ "$CI_PIPELINE_SOURCE" = "merge_request_event" ]; then 14 | echo "$CI_MERGE_REQUEST_TITLE" | commitlint -x @commitlint/config-conventional 15 | fi 16 | stage: ci 17 | variables: 18 | GIT_STRATEGY: none 19 | -------------------------------------------------------------------------------- /.github/workflows/readthedocs-preview.yml: -------------------------------------------------------------------------------- 1 | name: Read the Docs Pull Request Preview 2 | 3 | on: 4 | pull_request_target: 5 | types: 6 | - opened 7 | paths: 8 | - .github/workflows/readthedocs-preview.yml 9 | - .readthedocs.yaml 10 | - Makefile 11 | - README.md 12 | - docs/** 13 | - pdm.dev.lock 14 | - pdm.lock 15 | 16 | permissions: 17 | pull-requests: write 18 | 19 | concurrency: 20 | cancel-in-progress: true 21 | group: ${{ github.workflow }}-${{ github.ref }} 22 | 23 | jobs: 24 | documentation-links: 25 | runs-on: ubuntu-24.04 26 | steps: 27 | - name: Add Read the Docs preview's link to pull request 28 | uses: readthedocs/actions/preview@b8bba1484329bda1a3abe986df7ebc80a8950333 # v1.5 29 | with: 30 | project-slug: ss-python 31 | -------------------------------------------------------------------------------- /template/{% if repo_platform == 'gitlab' or repo_platform == 'gitlab-self-managed' %}.gitlab{% endif %}/workflows/commitlint.yml: -------------------------------------------------------------------------------- 1 | commitlint: 2 | image: 3 | name: commitlint/commitlint:20.1.0@sha256:caf971bc6ab2744d20f9967df648f210d6e79dda4055748d183d31700986f115 4 | entrypoint: [''] 5 | interruptible: true 6 | rules: 7 | - if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH && $CI_PIPELINE_SOURCE == "push" 8 | - if: $CI_PIPELINE_SOURCE == 'merge_request_event' 9 | script: 10 | - | 11 | if [ "$CI_PIPELINE_SOURCE" = "push" ]; then 12 | echo "$CI_COMMIT_TITLE" | commitlint -x @commitlint/config-conventional 13 | elif [ "$CI_PIPELINE_SOURCE" = "merge_request_event" ]; then 14 | echo "$CI_MERGE_REQUEST_TITLE" | commitlint -x @commitlint/config-conventional 15 | fi 16 | stage: ci 17 | variables: 18 | GIT_STRATEGY: none 19 | -------------------------------------------------------------------------------- /template/{% if repo_platform == 'github' %}.github{% endif %}/workflows/readthedocs-preview.yml.jinja: -------------------------------------------------------------------------------- 1 | name: Read the Docs Pull Request Preview 2 | 3 | on: 4 | pull_request_target: 5 | types: 6 | - opened 7 | paths: 8 | - .github/workflows/readthedocs-preview.yml 9 | - .readthedocs.yaml 10 | - Makefile 11 | - README.md 12 | - docs/** 13 | - pdm.dev.lock 14 | - pdm.lock 15 | 16 | permissions: 17 | pull-requests: write 18 | 19 | concurrency: 20 | cancel-in-progress: true 21 | group: {{ '${{ github.workflow }}-${{ github.ref }}' }} 22 | 23 | jobs: 24 | documentation-links: 25 | runs-on: ubuntu-24.04 26 | steps: 27 | - name: Add Read the Docs preview's link to pull request 28 | uses: readthedocs/actions/preview@b8bba1484329bda1a3abe986df7ebc80a8950333 # v1.5 29 | with: 30 | project-slug: {{ repo_name }} 31 | -------------------------------------------------------------------------------- /includes/version_compare.jinja: -------------------------------------------------------------------------------- 1 | {% macro version_higher_than(version1, version2) %} 2 | {{ "1" if version1.split(".") | map("int") | list >= version2.split(".") | map("int") | list }} 3 | {%- endmacro %} 4 | 5 | {% macro version_higher_than_validator(version1, version2) %} 6 | {{ 7 | "Invalid version. The version '%s' is not higher than '%s'." % (version1, version2) 8 | if not version_higher_than(version1, version2) 9 | }} 10 | {%- endmacro %} 11 | 12 | {% macro version_between(version, version_min, version_max) %} 13 | {{ 14 | "1" if version_min.split(".") | map("int") | list <= version.split(".") | map("int") | list <= version_max.split(".") | map("int") | list 15 | }} 16 | {%- endmacro %} 17 | 18 | {% macro version_between_validator(version, version_min, version_max) %} 19 | {{ 20 | "Invalid version. The version '%s' is not between '%s' and '%s'." % (version, version_min, version_max) 21 | if not version_between(version, version_min, version_max) 22 | }} 23 | {%- endmacro %} 24 | -------------------------------------------------------------------------------- /.gitlab/workflows/renovate.yml: -------------------------------------------------------------------------------- 1 | renovate: 2 | cache: 3 | key: ${CI_COMMIT_REF_SLUG}-renovate 4 | paths: 5 | - renovate/cache/renovate/repository/ 6 | image: renovate/renovate:42.30.2@sha256:582aa8b54c8f094fa57249ce5f143ee52f2e6f56c0952dc9a16e8dd3cd37586b 7 | rules: 8 | - if: $CI_PIPELINE_SOURCE == "schedule" && $PAT != null 9 | script: renovate $RENOVATE_EXTRA_FLAG 10 | stage: build 11 | variables: 12 | GIT_STRATEGY: none 13 | LOG_LEVEL: debug 14 | RENOVATE_ALLOWED_POST_UPGRADE_COMMANDS: '["^git", "^pdm", "^pip", "^copier", "^find"]' 15 | RENOVATE_BASE_DIR: $CI_PROJECT_DIR/renovate 16 | RENOVATE_BRANCH_PREFIX: renovate-gitlab/ 17 | RENOVATE_ENABLED_MANAGERS: '["pep621", "gitlabci", "regex", "pre-commit"]' 18 | RENOVATE_ENDPOINT: $CI_API_V4_URL 19 | RENOVATE_OPTIMIZE_FOR_DISABLED: "true" 20 | RENOVATE_PLATFORM: gitlab 21 | RENOVATE_REPOSITORIES: '["$CI_PROJECT_PATH"]' 22 | RENOVATE_REPOSITORY_CACHE: enabled 23 | RENOVATE_TOKEN: $PAT 24 | -------------------------------------------------------------------------------- /.github/workflows/commitlint.yml: -------------------------------------------------------------------------------- 1 | name: CommitLint 2 | 3 | on: 4 | pull_request: 5 | types: 6 | - opened 7 | - synchronize 8 | - reopened 9 | - edited 10 | push: 11 | branches: 12 | - main 13 | 14 | permissions: 15 | contents: read 16 | 17 | concurrency: 18 | cancel-in-progress: true 19 | group: ${{ github.workflow }}-${{ github.ref }} 20 | 21 | jobs: 22 | commitlint: 23 | container: 24 | image: commitlint/commitlint:20.1.0@sha256:caf971bc6ab2744d20f9967df648f210d6e79dda4055748d183d31700986f115 25 | runs-on: ubuntu-24.04 26 | steps: 27 | - run: env | sort 28 | - name: Validate the latest commit message with commitlint 29 | if: github.event_name == 'push' 30 | run: echo "${{ github.event.head_commit.message }}" | npx commitlint -x @commitlint/config-conventional 31 | - name: Validate pull request title with commitlint 32 | if: github.event_name == 'pull_request' 33 | run: echo "${{ github.event.pull_request.title }}" | npx commitlint -x @commitlint/config-conventional 34 | -------------------------------------------------------------------------------- /template/{% if repo_platform == 'gitlab' or repo_platform == 'gitlab-self-managed' %}.gitlab{% endif %}/workflows/renovate.yml: -------------------------------------------------------------------------------- 1 | renovate: 2 | cache: 3 | key: ${CI_COMMIT_REF_SLUG}-renovate 4 | paths: 5 | - renovate/cache/renovate/repository/ 6 | image: renovate/renovate:42.30.2@sha256:582aa8b54c8f094fa57249ce5f143ee52f2e6f56c0952dc9a16e8dd3cd37586b 7 | rules: 8 | - if: $CI_PIPELINE_SOURCE == "schedule" && $PAT != null 9 | script: renovate $RENOVATE_EXTRA_FLAG 10 | stage: build 11 | variables: 12 | GIT_STRATEGY: none 13 | LOG_LEVEL: debug 14 | RENOVATE_ALLOWED_POST_UPGRADE_COMMANDS: '["^git", "^pdm", "^pip", "^copier", "^find"]' 15 | RENOVATE_BASE_DIR: $CI_PROJECT_DIR/renovate 16 | RENOVATE_BRANCH_PREFIX: renovate-gitlab/ 17 | RENOVATE_ENABLED_MANAGERS: '["pep621", "gitlabci", "regex", "pre-commit"]' 18 | RENOVATE_ENDPOINT: $CI_API_V4_URL 19 | RENOVATE_OPTIMIZE_FOR_DISABLED: "true" 20 | RENOVATE_PLATFORM: gitlab 21 | RENOVATE_REPOSITORIES: '["$CI_PROJECT_PATH"]' 22 | RENOVATE_REPOSITORY_CACHE: enabled 23 | RENOVATE_TOKEN: $PAT 24 | -------------------------------------------------------------------------------- /template/{% if repo_platform == 'github' %}.github{% endif %}/workflows/commitlint.yml: -------------------------------------------------------------------------------- 1 | name: CommitLint 2 | 3 | on: 4 | pull_request: 5 | types: 6 | - opened 7 | - synchronize 8 | - reopened 9 | - edited 10 | push: 11 | branches: 12 | - main 13 | 14 | permissions: 15 | contents: read 16 | 17 | concurrency: 18 | cancel-in-progress: true 19 | group: ${{ github.workflow }}-${{ github.ref }} 20 | 21 | jobs: 22 | commitlint: 23 | container: 24 | image: commitlint/commitlint:20.1.0@sha256:caf971bc6ab2744d20f9967df648f210d6e79dda4055748d183d31700986f115 25 | runs-on: ubuntu-24.04 26 | steps: 27 | - run: env | sort 28 | - name: Validate the latest commit message with commitlint 29 | if: github.event_name == 'push' 30 | run: echo "${{ github.event.head_commit.message }}" | npx commitlint -x @commitlint/config-conventional 31 | - name: Validate pull request title with commitlint 32 | if: github.event_name == 'pull_request' 33 | run: echo "${{ github.event.pull_request.title }}" | npx commitlint -x @commitlint/config-conventional 34 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022-2025 Serious Scaffold 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /includes/licenses/MIT License.jinja: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) {{ copyright_year }} {{ copyright_holder }} 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /docs/development/cleanup-dev-env.md: -------------------------------------------------------------------------------- 1 | # Clean Up Development Environment 2 | 3 | When encountering environment-related problems, a straightforward solution is to cleanup the environment and setup a new one. Three different levels of cleanup approach are provided here. 4 | 5 | ## Intermediate cleanup 6 | 7 | Intermediate cleanup only removes common intermediate files, such as generated documentation, package, coverage report, cache files for mypy, pytest, ruff and so on. 8 | 9 | ```bash 10 | make clean 11 | ``` 12 | 13 | ## Deep cleanup 14 | 15 | Deep cleanup removes the pre-commit hook and the virtual environment alongside the common intermediate files. 16 | 17 | ```bash 18 | make deepclean 19 | ``` 20 | 21 | ## Complete cleanup 22 | 23 | Complete cleanup restores the repository to its original, freshly-cloned state, ideal for starting over from scratch. 24 | 25 | ```{caution} 26 | This will remove all untracked files, please use it with caution. It is recommended to check with dry-run mode (`git clean -dfnx`) before actually removing anything. For more information, please refer to the [git-clean documentation](https://git-scm.com/docs/git-clean). 27 | ``` 28 | 29 | ```bash 30 | git clean -dfx 31 | ``` 32 | -------------------------------------------------------------------------------- /template/docs/development/cleanup-dev-env.md: -------------------------------------------------------------------------------- 1 | # Clean Up Development Environment 2 | 3 | When encountering environment-related problems, a straightforward solution is to cleanup the environment and setup a new one. Three different levels of cleanup approach are provided here. 4 | 5 | ## Intermediate cleanup 6 | 7 | Intermediate cleanup only removes common intermediate files, such as generated documentation, package, coverage report, cache files for mypy, pytest, ruff and so on. 8 | 9 | ```bash 10 | make clean 11 | ``` 12 | 13 | ## Deep cleanup 14 | 15 | Deep cleanup removes the pre-commit hook and the virtual environment alongside the common intermediate files. 16 | 17 | ```bash 18 | make deepclean 19 | ``` 20 | 21 | ## Complete cleanup 22 | 23 | Complete cleanup restores the repository to its original, freshly-cloned state, ideal for starting over from scratch. 24 | 25 | ```{caution} 26 | This will remove all untracked files, please use it with caution. It is recommended to check with dry-run mode (`git clean -dfnx`) before actually removing anything. For more information, please refer to the [git-clean documentation](https://git-scm.com/docs/git-clean). 27 | ``` 28 | 29 | ```bash 30 | git clean -dfx 31 | ``` 32 | -------------------------------------------------------------------------------- /.github/workflows/delete-untagged-packages.yml: -------------------------------------------------------------------------------- 1 | name: Delete Untagged Packages 2 | 3 | on: 4 | schedule: 5 | - cron: "0 2 * * 0" 6 | workflow_dispatch: null 7 | 8 | permissions: 9 | packages: write 10 | 11 | jobs: 12 | delete-untagged-packages: 13 | runs-on: ubuntu-24.04 14 | steps: 15 | - name: Delete untagged dev-cache packages 16 | uses: actions/delete-package-versions@e5bc658cc4c965c472efe991f8beea3981499c55 # v5.0.0 17 | with: 18 | package-name: "ss-python/dev-cache" 19 | package-type: "container" 20 | delete-only-untagged-versions: "true" 21 | - name: Delete untagged development packages 22 | uses: actions/delete-package-versions@e5bc658cc4c965c472efe991f8beea3981499c55 # v5.0.0 23 | with: 24 | package-name: "ss-python/dev" 25 | package-type: "container" 26 | delete-only-untagged-versions: "true" 27 | - name: Delete untagged production packages 28 | uses: actions/delete-package-versions@e5bc658cc4c965c472efe991f8beea3981499c55 # v5.0.0 29 | with: 30 | package-name: "ss-python" 31 | package-type: "container" 32 | delete-only-untagged-versions: "true" 33 | -------------------------------------------------------------------------------- /src/ss_python/settings.py: -------------------------------------------------------------------------------- 1 | """Settings Module.""" 2 | 3 | import logging 4 | from logging import getLevelName 5 | from typing import Optional 6 | 7 | from pydantic_settings import BaseSettings, SettingsConfigDict 8 | 9 | 10 | class GlobalSettings(BaseSettings): 11 | """System level settings.""" 12 | 13 | ci: bool = False 14 | """Indicator for whether or not in CI/CD environment.""" 15 | 16 | 17 | class Settings(BaseSettings): 18 | """Project specific settings.""" 19 | 20 | logging_level: Optional[str] = getLevelName(logging.INFO) 21 | """Default logging level for the project.""" 22 | 23 | model_config = SettingsConfigDict( 24 | env_prefix="SS_PYTHON_", 25 | ) 26 | 27 | 28 | # NOTE(huxuan): `#:` style docstring is required for module attributes to satisfy both 29 | # autodoc [1] and `check-docstring-first` in `pre-commit` [2]. 30 | # [1] https://www.sphinx-doc.org/en/master/usage/extensions/autodoc.html#directive-autoattribute 31 | # [2] https://github.com/pre-commit/pre-commit-hooks/issues/159#issuecomment-559886109 32 | 33 | #: Instance for system level settings. 34 | global_settings = GlobalSettings() 35 | 36 | #: Instance for project specific settings. 37 | settings = Settings() 38 | -------------------------------------------------------------------------------- /docs/development/setup-dev-env.md: -------------------------------------------------------------------------------- 1 | # Set Up Development Environment 2 | 3 | This page shows the approach to set up development environment. To simplify the process, a unified `Makefile` is maintained at the root directory of the repo. In other words, all the `make` related commands are supposed to run there. 4 | 5 | ## Prerequisites 6 | 7 | [pipx](https://pipx.pypa.io/) is required to manage the standalone tools used across the development lifecycle. 8 | Please refer to pipx's installation instructions [here](https://pipx.pypa.io/stable/installation/). 9 | Once pipx is set up, install the needed standalone tools with the following command: 10 | 11 | ```bash 12 | make prerequisites 13 | ``` 14 | 15 | ## Setup 16 | 17 | Development environment can be setup with the following command: 18 | 19 | ```bash 20 | make dev 21 | ``` 22 | 23 | This command will accomplish the following tasks: 24 | 25 | - Create a virtual environment. 26 | - Install all the dependencies, including those for documentation, lint, package and test. 27 | - Install the project in editable mode. 28 | - Install git hook scripts for `pre-commit`. 29 | 30 | To speed up the setup process in certain scenarios, you may find helpful. 31 | -------------------------------------------------------------------------------- /template/docs/development/setup-dev-env.md: -------------------------------------------------------------------------------- 1 | # Set Up Development Environment 2 | 3 | This page shows the approach to set up development environment. To simplify the process, a unified `Makefile` is maintained at the root directory of the repo. In other words, all the `make` related commands are supposed to run there. 4 | 5 | ## Prerequisites 6 | 7 | [pipx](https://pipx.pypa.io/) is required to manage the standalone tools used across the development lifecycle. 8 | Please refer to pipx's installation instructions [here](https://pipx.pypa.io/stable/installation/). 9 | Once pipx is set up, install the needed standalone tools with the following command: 10 | 11 | ```bash 12 | make prerequisites 13 | ``` 14 | 15 | ## Setup 16 | 17 | Development environment can be setup with the following command: 18 | 19 | ```bash 20 | make dev 21 | ``` 22 | 23 | This command will accomplish the following tasks: 24 | 25 | - Create a virtual environment. 26 | - Install all the dependencies, including those for documentation, lint, package and test. 27 | - Install the project in editable mode. 28 | - Install git hook scripts for `pre-commit`. 29 | 30 | To speed up the setup process in certain scenarios, you may find helpful. 31 | -------------------------------------------------------------------------------- /docs/management/init.md: -------------------------------------------------------------------------------- 1 | # Project Initialization 2 | 3 | ## Prerequisites 4 | 5 | [pipx](https://pipx.pypa.io/) is required to manage the standalone tools used across the development lifecycle. 6 | Please refer to pipx's installation instructions [here](https://pipx.pypa.io/stable/installation/). 7 | Once pipx is set up, install the copier for project generation using the following command: 8 | 9 | ```bash 10 | pipx install copier==9.8.0 11 | ``` 12 | 13 | ## Create the Repository 14 | 15 | Create a blank Git repository on the hosting platform. Clone it locally and navigate to the root directory: 16 | 17 | ```bash 18 | git clone git@github.com:serious-scaffold/ss-python.git 19 | cd ss-python 20 | ``` 21 | 22 | ## Generate the Project 23 | 24 | Running the following command and answer the prompts to set up the project: 25 | 26 | ```bash 27 | copier copy gh:serious-scaffold/ss-python . 28 | ``` 29 | 30 | ## Set Up Development Environment 31 | 32 | Set up development environment to prepare for the initial commit: 33 | 34 | ```bash 35 | make dev 36 | ``` 37 | 38 | ## Commit and push 39 | 40 | ```bash 41 | git add . 42 | git commit -m "chore: init from serious-scaffold-python" 43 | SKIP=no-commit-to-branch git push 44 | ``` 45 | 46 | Now, everything is done! 47 | -------------------------------------------------------------------------------- /template/{% if repo_platform == 'github' %}.github{% endif %}/workflows/delete-untagged-packages.yml.jinja: -------------------------------------------------------------------------------- 1 | name: Delete Untagged Packages 2 | 3 | on: 4 | schedule: 5 | - cron: "0 2 * * 0" 6 | workflow_dispatch: null 7 | 8 | permissions: 9 | packages: write 10 | 11 | jobs: 12 | delete-untagged-packages: 13 | runs-on: ubuntu-24.04 14 | steps: 15 | - name: Delete untagged dev-cache packages 16 | uses: actions/delete-package-versions@e5bc658cc4c965c472efe991f8beea3981499c55 # v5.0.0 17 | with: 18 | package-name: "{{ repo_name }}/dev-cache" 19 | package-type: "container" 20 | delete-only-untagged-versions: "true" 21 | - name: Delete untagged development packages 22 | uses: actions/delete-package-versions@e5bc658cc4c965c472efe991f8beea3981499c55 # v5.0.0 23 | with: 24 | package-name: "{{ repo_name }}/dev" 25 | package-type: "container" 26 | delete-only-untagged-versions: "true" 27 | - name: Delete untagged production packages 28 | uses: actions/delete-package-versions@e5bc658cc4c965c472efe991f8beea3981499c55 # v5.0.0 29 | with: 30 | package-name: "{{ repo_name }}" 31 | package-type: "container" 32 | delete-only-untagged-versions: "true" 33 | -------------------------------------------------------------------------------- /includes/licenses/The Unlicense (Unlicense).jinja: -------------------------------------------------------------------------------- 1 | This is free and unencumbered software released into the public domain. 2 | 3 | Anyone is free to copy, modify, publish, use, compile, sell, or 4 | distribute this software, either in source code form or as a compiled 5 | binary, for any purpose, commercial or non-commercial, and by any 6 | means. 7 | 8 | In jurisdictions that recognize copyright laws, the author or authors 9 | of this software dedicate any and all copyright interest in the 10 | software to the public domain. We make this dedication for the benefit 11 | of the public at large and to the detriment of our heirs and 12 | successors. We intend this dedication to be an overt act of 13 | relinquishment in perpetuity of all present and future rights to this 14 | software under copyright law. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 19 | IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR 20 | OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, 21 | ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 22 | OTHER DEALINGS IN THE SOFTWARE. 23 | 24 | For more information, please refer to 25 | -------------------------------------------------------------------------------- /template/docs/management/init.md.jinja: -------------------------------------------------------------------------------- 1 | {% from pathjoin("includes", "variable.jinja") import repo_url with context %} 2 | # Project Initialization 3 | 4 | ## Prerequisites 5 | 6 | [pipx](https://pipx.pypa.io/) is required to manage the standalone tools used across the development lifecycle. 7 | Please refer to pipx's installation instructions [here](https://pipx.pypa.io/stable/installation/). 8 | Once pipx is set up, install the copier for project generation using the following command: 9 | 10 | ```bash 11 | pipx install copier==9.8.0 12 | ``` 13 | 14 | ## Create the Repository 15 | 16 | Create a blank Git repository on the hosting platform. Clone it locally and navigate to the root directory: 17 | 18 | ```bash 19 | git clone git@{{ repo_host }}:{{ repo_namespace }}/{{ repo_name }}.git 20 | cd {{ repo_name }} 21 | ``` 22 | 23 | ## Generate the Project 24 | 25 | Running the following command and answer the prompts to set up the project: 26 | 27 | ```bash 28 | copier copy gh:serious-scaffold/ss-python . 29 | ``` 30 | 31 | ## Set Up Development Environment 32 | 33 | Set up development environment to prepare for the initial commit: 34 | 35 | ```bash 36 | make dev 37 | ``` 38 | 39 | ## Commit and push 40 | 41 | ```bash 42 | git add . 43 | git commit -m "chore: init from serious-scaffold-python" 44 | SKIP=no-commit-to-branch git push 45 | ``` 46 | 47 | Now, everything is done! 48 | -------------------------------------------------------------------------------- /.gitlab/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | ci: 2 | artifacts: 3 | reports: 4 | coverage_report: 5 | coverage_format: cobertura 6 | path: coverage.xml 7 | cache: 8 | paths: 9 | - .venv 10 | key: 11 | files: 12 | - pdm.dev.lock 13 | - pdm.lock 14 | prefix: venv-${PYTHON_VERSION} 15 | coverage: /TOTAL.*? (100(?:\.0+)?\%|[1-9]?\d(?:\.\d+)?\%)$/ 16 | image: ${CI_REGISTRY_IMAGE}/dev:py${PYTHON_VERSION} 17 | interruptible: true 18 | parallel: 19 | matrix: 20 | - PYTHON_VERSION: 21 | - '3.9' 22 | - '3.10' 23 | - '3.11' 24 | - '3.12' 25 | - '3.13' 26 | rules: 27 | - if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH && $CI_PIPELINE_SOURCE == "push" 28 | - if: $CI_PIPELINE_SOURCE == 'merge_request_event' 29 | script: 30 | - make prerequisites 31 | - make dev 32 | - make lint test doc build 33 | stage: ci 34 | consistency: 35 | interruptible: true 36 | rules: 37 | - if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH && $CI_PIPELINE_SOURCE == "push" 38 | - if: $CI_PIPELINE_SOURCE == 'merge_request_event' 39 | script: 40 | - git config --global user.name gitlab-ci 41 | - git config --global user.email gitlab-ci@gitlab.com 42 | - pipx install copier==9.8.0 43 | - make template-build 44 | - git diff 45 | - git status --porcelain 46 | - test -z "$(git status --porcelain)" 47 | stage: ci 48 | -------------------------------------------------------------------------------- /.readthedocs.yaml: -------------------------------------------------------------------------------- 1 | build: 2 | apt_packages: 3 | - pipx 4 | jobs: 5 | post_checkout: 6 | - git fetch --unshallow || true 7 | # Cancel building pull requests when there aren't changed in the related files and folders. 8 | # If there are no changes (git diff exits with 0) we force the command to return with 183. 9 | # This is a special exit code on Read the Docs that will cancel the build immediately. 10 | # Ref: https://docs.readthedocs.io/en/stable/build-customization.html#cancel-build-based-on-a-condition 11 | - | 12 | if [ "$READTHEDOCS_VERSION_TYPE" = "external" ] && git diff --quiet origin/main -- \ 13 | .github/workflows/readthedocs-preview.yml \ 14 | .readthedocs.yaml \ 15 | Makefile \ 16 | README.md \ 17 | docs/ \ 18 | pdm.dev.lock \ 19 | pdm.lock; 20 | then 21 | exit 183; 22 | fi 23 | post_system_dependencies: 24 | - env | sort 25 | pre_create_environment: 26 | - PIPX_BIN_DIR=$READTHEDOCS_VIRTUALENV_PATH/bin pipx install pdm==2.25.4 27 | post_install: 28 | - VIRTUAL_ENV=$READTHEDOCS_VIRTUALENV_PATH make dev-doc 29 | post_build: 30 | - VIRTUAL_ENV=$READTHEDOCS_VIRTUALENV_PATH make mypy doc-coverage 31 | os: ubuntu-24.04 32 | tools: 33 | python: "3.12" 34 | sphinx: 35 | configuration: docs/conf.py 36 | fail_on_warning: true 37 | version: 2 38 | -------------------------------------------------------------------------------- /template/.readthedocs.yaml.jinja: -------------------------------------------------------------------------------- 1 | build: 2 | apt_packages: 3 | - pipx 4 | jobs: 5 | post_checkout: 6 | - git fetch --unshallow || true 7 | # Cancel building pull requests when there aren't changed in the related files and folders. 8 | # If there are no changes (git diff exits with 0) we force the command to return with 183. 9 | # This is a special exit code on Read the Docs that will cancel the build immediately. 10 | # Ref: https://docs.readthedocs.io/en/stable/build-customization.html#cancel-build-based-on-a-condition 11 | - | 12 | if [ "$READTHEDOCS_VERSION_TYPE" = "external" ] && git diff --quiet origin/main -- \ 13 | .github/workflows/readthedocs-preview.yml \ 14 | .readthedocs.yaml \ 15 | Makefile \ 16 | README.md \ 17 | docs/ \ 18 | pdm.dev.lock \ 19 | pdm.lock; 20 | then 21 | exit 183; 22 | fi 23 | post_system_dependencies: 24 | - env | sort 25 | pre_create_environment: 26 | - PIPX_BIN_DIR=$READTHEDOCS_VIRTUALENV_PATH/bin pipx install pdm==2.25.4 27 | post_install: 28 | - VIRTUAL_ENV=$READTHEDOCS_VIRTUALENV_PATH make dev-doc 29 | post_build: 30 | - VIRTUAL_ENV=$READTHEDOCS_VIRTUALENV_PATH make mypy doc-coverage 31 | os: ubuntu-24.04 32 | tools: 33 | python: "{{ default_py }}" 34 | sphinx: 35 | configuration: docs/conf.py 36 | fail_on_warning: true 37 | version: 2 38 | -------------------------------------------------------------------------------- /includes/licenses/Boost Software License 1.0 (BSL-1.0).jinja: -------------------------------------------------------------------------------- 1 | Boost Software License - Version 1.0 - August 17th, 2003 2 | 3 | Permission is hereby granted, free of charge, to any person or organization 4 | obtaining a copy of the software and accompanying documentation covered by 5 | this license (the "Software") to use, reproduce, display, distribute, 6 | execute, and transmit the Software, and to prepare derivative works of the 7 | Software, and to permit third-parties to whom the Software is furnished to 8 | do so, all subject to the following: 9 | 10 | The copyright notices in the Software and this entire statement, including 11 | the above license grant, this restriction and the following disclaimer, 12 | must be included in all copies of the Software, in whole or in part, and 13 | all derivative works of the Software, unless such copies or derivative 14 | works are solely in the form of machine-executable object code generated by 15 | a source language processor. 16 | 17 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | FITNESS FOR A PARTICULAR PURPOSE, TITLE AND NON-INFRINGEMENT. IN NO EVENT 20 | SHALL THE COPYRIGHT HOLDERS OR ANYONE DISTRIBUTING THE SOFTWARE BE LIABLE 21 | FOR ANY DAMAGES OR OTHER LIABILITY, WHETHER IN CONTRACT, TORT OR OTHERWISE, 22 | ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 23 | DEALINGS IN THE SOFTWARE. 24 | -------------------------------------------------------------------------------- /docs/advanced/partial-dev-env.md: -------------------------------------------------------------------------------- 1 | # Partially Set Up Development Environment 2 | 3 | In certain cases, it is unnecessary to install all dependencies as well as the pre-commit hook. For example, this can speed up the setup process in CI/CD. 4 | 5 | ## Minimal installation 6 | 7 | Install the project in editable mode with only the necessary dependencies, which is useful for scenarios like deployment. 8 | 9 | ```bash 10 | make install 11 | ``` 12 | 13 | ## Documentation generation 14 | 15 | Install the project in editable mode with dependencies related to `doc`, 16 | recommended for scenarios like the documentation generation CI/CD process. 17 | 18 | ```bash 19 | make dev-doc 20 | ``` 21 | 22 | ## Lint check 23 | 24 | Install the project in editable mode with dependencies related to `lint`, 25 | recommended for scenarios like the lint CI/CD process. 26 | 27 | ```bash 28 | make dev-lint 29 | ``` 30 | 31 | ## Package build 32 | 33 | Install the project in editable mode with dependencies related to `package`, 34 | recommended for scenarios like the package CI/CD process. 35 | 36 | ```bash 37 | make dev-package 38 | ``` 39 | 40 | ## Testing 41 | 42 | Install the project in editable mode with dependencies related to `test`, 43 | recommended for scenarios like the test CI/CD process. 44 | 45 | ```bash 46 | make dev-test 47 | ``` 48 | 49 | ## Combination 50 | 51 | To install dependencies for `doc` and `lint`, use the following command: 52 | 53 | ```bash 54 | make dev-doc,lint 55 | ``` 56 | -------------------------------------------------------------------------------- /template/docs/advanced/partial-dev-env.md: -------------------------------------------------------------------------------- 1 | # Partially Set Up Development Environment 2 | 3 | In certain cases, it is unnecessary to install all dependencies as well as the pre-commit hook. For example, this can speed up the setup process in CI/CD. 4 | 5 | ## Minimal installation 6 | 7 | Install the project in editable mode with only the necessary dependencies, which is useful for scenarios like deployment. 8 | 9 | ```bash 10 | make install 11 | ``` 12 | 13 | ## Documentation generation 14 | 15 | Install the project in editable mode with dependencies related to `doc`, 16 | recommended for scenarios like the documentation generation CI/CD process. 17 | 18 | ```bash 19 | make dev-doc 20 | ``` 21 | 22 | ## Lint check 23 | 24 | Install the project in editable mode with dependencies related to `lint`, 25 | recommended for scenarios like the lint CI/CD process. 26 | 27 | ```bash 28 | make dev-lint 29 | ``` 30 | 31 | ## Package build 32 | 33 | Install the project in editable mode with dependencies related to `package`, 34 | recommended for scenarios like the package CI/CD process. 35 | 36 | ```bash 37 | make dev-package 38 | ``` 39 | 40 | ## Testing 41 | 42 | Install the project in editable mode with dependencies related to `test`, 43 | recommended for scenarios like the test CI/CD process. 44 | 45 | ```bash 46 | make dev-test 47 | ``` 48 | 49 | ## Combination 50 | 51 | To install dependencies for `doc` and `lint`, use the following command: 52 | 53 | ```bash 54 | make dev-doc,lint 55 | ``` 56 | -------------------------------------------------------------------------------- /template/src/{{ module_name }}/settings.py.jinja: -------------------------------------------------------------------------------- 1 | {% from pathjoin("includes", "version_compare.jinja") import version_higher_than %} 2 | """Settings Module.""" 3 | 4 | import logging 5 | from logging import getLevelName 6 | {% if not version_higher_than(min_py, "3.10") %} 7 | from typing import Optional 8 | {% endif %} 9 | 10 | from pydantic_settings import BaseSettings, SettingsConfigDict 11 | 12 | 13 | class GlobalSettings(BaseSettings): 14 | """System level settings.""" 15 | 16 | ci: bool = False 17 | """Indicator for whether or not in CI/CD environment.""" 18 | 19 | 20 | class Settings(BaseSettings): 21 | """Project specific settings.""" 22 | 23 | {% if not version_higher_than(min_py, "3.10") %} 24 | logging_level: Optional[str] = getLevelName(logging.INFO) 25 | {% else %} 26 | logging_level: str | None = getLevelName(logging.INFO) 27 | {% endif %} 28 | """Default logging level for the project.""" 29 | 30 | model_config = SettingsConfigDict( 31 | env_prefix="{{ module_name|upper }}_", 32 | ) 33 | 34 | 35 | # NOTE(huxuan): `#:` style docstring is required for module attributes to satisfy both 36 | # autodoc [1] and `check-docstring-first` in `pre-commit` [2]. 37 | # [1] https://www.sphinx-doc.org/en/master/usage/extensions/autodoc.html#directive-autoattribute 38 | # [2] https://github.com/pre-commit/pre-commit-hooks/issues/159#issuecomment-559886109 39 | 40 | #: Instance for system level settings. 41 | global_settings = GlobalSettings() 42 | 43 | #: Instance for project specific settings. 44 | settings = Settings() 45 | -------------------------------------------------------------------------------- /docs/development/tests.md: -------------------------------------------------------------------------------- 1 | # Tests 2 | 3 | In the context of CI/CD automation, dependency updates, and the release process, tests play a crucial role in daily development. We utilize [pytest](https://docs.pytest.org/) and [coverage](https://coverage.readthedocs.io) with proper configuration to ensure everything works as expected. This page provides general information and conventions we wish you to follow. 4 | 5 | ## Running Tests 6 | 7 | After [setting up the development environment](/development/setup-dev-env.md), tests can be run with the command: 8 | 9 | ```bash 10 | make test 11 | ``` 12 | 13 | With the default configuration, this command displays the result for each test case, the execution time for slow test cases, and a report on test coverage. 14 | 15 | ## Writing Tests 16 | 17 | For guidelines on how to write tests, refer to [the official documentation](https://docs.pytest.org/how-to/assert.html). Here are some conventions we expect you to follow: 18 | 19 | 1. Organize all test cases under the `tests` directory. 20 | 2. Align test modules with the modules to be tested. 21 | 22 | For example, tests for the `ss_python.cli` module should be located in the file `tests/cli_test.py`. If there are too many test cases, they can be split into files within the `tests/cli/` directory, using a prefix for each test file. 23 | 3. Unless necessary, do not lower the threshold of the test coverage. 24 | 25 | ## Coverage Report 26 | 27 | After running the tests, the coverage report will be printed on the screen and generated as part of the documentation. You can view it [here](/reports/coverage/index.md). 28 | -------------------------------------------------------------------------------- /template/docs/development/tests.md.jinja: -------------------------------------------------------------------------------- 1 | # Tests 2 | 3 | In the context of CI/CD automation, dependency updates, and the release process, tests play a crucial role in daily development. We utilize [pytest](https://docs.pytest.org/) and [coverage](https://coverage.readthedocs.io) with proper configuration to ensure everything works as expected. This page provides general information and conventions we wish you to follow. 4 | 5 | ## Running Tests 6 | 7 | After [setting up the development environment](/development/setup-dev-env.md), tests can be run with the command: 8 | 9 | ```bash 10 | make test 11 | ``` 12 | 13 | With the default configuration, this command displays the result for each test case, the execution time for slow test cases, and a report on test coverage. 14 | 15 | ## Writing Tests 16 | 17 | For guidelines on how to write tests, refer to [the official documentation](https://docs.pytest.org/how-to/assert.html). Here are some conventions we expect you to follow: 18 | 19 | 1. Organize all test cases under the `tests` directory. 20 | 2. Align test modules with the modules to be tested. 21 | 22 | For example, tests for the `{{ module_name}}.cli` module should be located in the file `tests/cli_test.py`. If there are too many test cases, they can be split into files within the `tests/cli/` directory, using a prefix for each test file. 23 | 3. Unless necessary, do not lower the threshold of the test coverage. 24 | 25 | ## Coverage Report 26 | 27 | After running the tests, the coverage report will be printed on the screen and generated as part of the documentation. You can view it [here](/reports/coverage/index.md). 28 | -------------------------------------------------------------------------------- /.devcontainer/devcontainer.json: -------------------------------------------------------------------------------- 1 | { 2 | "customizations": { 3 | // Configure extensions specific to VS Code. 4 | "vscode": { 5 | "extensions": [ 6 | "DavidAnson.vscode-markdownlint", 7 | "ExecutableBookProject.myst-highlight", 8 | "charliermarsh.ruff", 9 | "ms-python.mypy-type-checker", 10 | "ms-python.python", 11 | "richie5um2.vscode-sort-json", 12 | "streetsidesoftware.code-spell-checker" 13 | ] 14 | } 15 | }, 16 | "image": "ghcr.io/serious-scaffold/ss-python/dev:py3.12", 17 | // Force the image update to ensure the latest version which might be a bug. 18 | // Reference: https://github.com/microsoft/vscode-remote-release/issues/9391 19 | "initializeCommand": "docker pull ghcr.io/serious-scaffold/ss-python/dev:py3.12", 20 | // Use a targeted named volume for .venv folder to improve disk performance. 21 | // Reference: https://code.visualstudio.com/remote/advancedcontainers/improve-performance#_use-a-targeted-named-volume 22 | "mounts": [ 23 | "source=${localWorkspaceFolderBasename}-venv,target=${containerWorkspaceFolder}/.venv,type=volume" 24 | ], 25 | "name": "ss-python", 26 | // Set proper permission for the .venv folder when the container created. 27 | "postCreateCommand": "sudo chown ss-python:ss-python .venv", 28 | // Prepare the development environment when the container starts. 29 | "postStartCommand": "make dev", 30 | // Use the non-root user in the container. 31 | "remoteUser": "ss-python" 32 | } 33 | -------------------------------------------------------------------------------- /template/.devcontainer/devcontainer.json.jinja: -------------------------------------------------------------------------------- 1 | { 2 | "customizations": { 3 | // Configure extensions specific to VS Code. 4 | "vscode": { 5 | "extensions": [ 6 | "DavidAnson.vscode-markdownlint", 7 | "ExecutableBookProject.myst-highlight", 8 | "charliermarsh.ruff", 9 | "ms-python.mypy-type-checker", 10 | "ms-python.python", 11 | "richie5um2.vscode-sort-json", 12 | "streetsidesoftware.code-spell-checker" 13 | ] 14 | } 15 | }, 16 | "image": "{{ container_registry_host }}/{{ repo_namespace }}/{{ repo_name }}/dev:py{{ default_py }}", 17 | // Force the image update to ensure the latest version which might be a bug. 18 | // Reference: https://github.com/microsoft/vscode-remote-release/issues/9391 19 | "initializeCommand": "docker pull {{ container_registry_host }}/{{ repo_namespace }}/{{ repo_name }}/dev:py{{ default_py }}", 20 | // Use a targeted named volume for .venv folder to improve disk performance. 21 | // Reference: https://code.visualstudio.com/remote/advancedcontainers/improve-performance#_use-a-targeted-named-volume 22 | "mounts": [ 23 | "source=${localWorkspaceFolderBasename}-venv,target=${containerWorkspaceFolder}/.venv,type=volume" 24 | ], 25 | "name": "{{ repo_name }}", 26 | // Set proper permission for the .venv folder when the container created. 27 | "postCreateCommand": "sudo chown {{ repo_name }}:{{ repo_name }} .venv", 28 | // Prepare the development environment when the container starts. 29 | "postStartCommand": "make dev", 30 | // Use the non-root user in the container. 31 | "remoteUser": "{{ repo_name }}" 32 | } 33 | -------------------------------------------------------------------------------- /template/{% if repo_platform == 'gitlab' or repo_platform == 'gitlab-self-managed' %}.gitlab{% endif %}/workflows/ci.yml.jinja: -------------------------------------------------------------------------------- 1 | {% from pathjoin("includes", "version_compare.jinja") import version_between %} 2 | ci: 3 | artifacts: 4 | reports: 5 | coverage_report: 6 | coverage_format: cobertura 7 | path: coverage.xml 8 | cache: 9 | paths: 10 | - .venv 11 | key: 12 | files: 13 | - pdm.dev.lock 14 | - pdm.lock 15 | prefix: venv-${PYTHON_VERSION} 16 | coverage: /TOTAL.*? (100(?:\.0+)?\%|[1-9]?\d(?:\.\d+)?\%)$/ 17 | image: ${CI_REGISTRY_IMAGE}/dev:py${PYTHON_VERSION} 18 | interruptible: true 19 | parallel: 20 | matrix: 21 | - PYTHON_VERSION: 22 | {% if version_between("3.9", min_py, max_py) %} 23 | - '3.9' 24 | {% endif %} 25 | {% if version_between("3.10", min_py, max_py) %} 26 | - '3.10' 27 | {% endif %} 28 | {% if version_between("3.11", min_py, max_py) %} 29 | - '3.11' 30 | {% endif %} 31 | {% if version_between("3.12", min_py, max_py) %} 32 | - '3.12' 33 | {% endif %} 34 | {% if version_between("3.13", min_py, max_py) %} 35 | - '3.13' 36 | {% endif %} 37 | rules: 38 | - if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH && $CI_PIPELINE_SOURCE == "push" 39 | - if: $CI_PIPELINE_SOURCE == 'merge_request_event' 40 | script: 41 | - make prerequisites 42 | - make dev 43 | - make lint test doc build 44 | stage: ci 45 | {% if project_name == 'Serious Scaffold Python' %} 46 | consistency: 47 | interruptible: true 48 | rules: 49 | - if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH && $CI_PIPELINE_SOURCE == "push" 50 | - if: $CI_PIPELINE_SOURCE == 'merge_request_event' 51 | script: 52 | - git config --global user.name gitlab-ci 53 | - git config --global user.email gitlab-ci@gitlab.com 54 | - pipx install copier==9.8.0 55 | - make template-build 56 | - git diff 57 | - git status --porcelain 58 | - test -z "$(git status --porcelain)" 59 | stage: ci 60 | {% endif %} 61 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | pull_request: 5 | push: 6 | branches: 7 | - main 8 | 9 | permissions: 10 | contents: read 11 | 12 | concurrency: 13 | cancel-in-progress: true 14 | group: ${{ github.workflow }}-${{ github.ref }} 15 | 16 | jobs: 17 | ci: 18 | if: ${{ !cancelled() && ! failure() }} 19 | runs-on: ${{ matrix.os }} 20 | steps: 21 | - name: Checkout repository 22 | uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 23 | with: 24 | fetch-depth: 0 25 | - name: Set up PDM 26 | uses: pdm-project/setup-pdm@94a823180e06fcde4ad29308721954a521c96ed0 # v4.4 27 | with: 28 | cache: true 29 | python-version: ${{ matrix.python-version }} 30 | version: 2.25.4 31 | cache-dependency-path: | 32 | ./pdm.dev.lock 33 | ./pdm.lock 34 | - run: env | sort 35 | - run: make prerequisites 36 | - run: make dev 37 | - run: make lint test doc build 38 | strategy: 39 | matrix: 40 | os: 41 | # renovate: github-runner 42 | - macos-14 43 | # renovate: github-runner 44 | - ubuntu-24.04 45 | # renovate: github-runner 46 | - windows-2025 47 | python-version: 48 | - "3.9" 49 | - "3.10" 50 | - "3.11" 51 | - "3.12" 52 | - "3.13" 53 | consistency: 54 | if: ${{ !cancelled() && ! failure() }} 55 | runs-on: ubuntu-24.04 56 | steps: 57 | - run: env | sort 58 | - name: Checkout repository 59 | uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 60 | - name: Set up Git 61 | run: | 62 | git config --global user.name github-actions 63 | git config --global user.email github-actions@github.com 64 | - run: pipx install copier==9.8.0 65 | - run: make template-build 66 | - run: git diff 67 | - run: git status --porcelain 68 | - run: test -z "$(git status --porcelain)" 69 | -------------------------------------------------------------------------------- /.github/workflows/semantic-release.yml: -------------------------------------------------------------------------------- 1 | name: Semantic Release 2 | 3 | on: 4 | workflow_run: 5 | workflows: [CI] 6 | types: [completed] 7 | branches: [main] 8 | 9 | jobs: 10 | semantic-release: 11 | name: Semantic Release 12 | runs-on: ubuntu-24.04 13 | # Ensure CI workflow is succeeded and avoid semantic release on forked repository 14 | if: github.event.workflow_run.conclusion == 'success' && github.repository == 'serious-scaffold/ss-python' 15 | permissions: 16 | contents: write 17 | id-token: write 18 | issues: write 19 | pull-requests: write 20 | steps: 21 | - id: generate-token 22 | name: Generate a token with GitHub App if App ID exists 23 | if: vars.BOT_APP_ID 24 | uses: actions/create-github-app-token@7e473efe3cb98aa54f8d4bac15400b15fad77d94 # v2.2.0 25 | with: 26 | app-id: ${{ vars.BOT_APP_ID }} 27 | private-key: ${{ secrets.BOT_PRIVATE_KEY }} 28 | - name: Warn if use GITHUB_TOKEN 29 | run: | 30 | if [ -z "${{ steps.generate-token.outputs.token || secrets.PAT }}" ]; then 31 | echo "# :warning: GITHUB_TOKEN is used for semantic-release" >> $GITHUB_STEP_SUMMARY 32 | echo "The GITHUB_TOKEN is used instead of a bot token or PAT and will not emit the released publish event for the released workflow." >> $GITHUB_STEP_SUMMARY 33 | fi 34 | - name: Checkout repository 35 | uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 36 | with: 37 | fetch-depth: 0 38 | persist-credentials: false 39 | - name: Setup Node.js 40 | uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6.0.0 41 | with: 42 | node-version: 'lts/*' 43 | - name: Semantic Release 44 | env: 45 | GITHUB_TOKEN: ${{ steps.generate-token.outputs.token || secrets.PAT || secrets.GITHUB_TOKEN }} 46 | run: > 47 | npx 48 | --package conventional-changelog-conventionalcommits@9.1.0 49 | --package semantic-release@25.0.2 50 | semantic-release 51 | -------------------------------------------------------------------------------- /template/{% if repo_platform == 'github' %}.github{% endif %}/workflows/semantic-release.yml.jinja: -------------------------------------------------------------------------------- 1 | name: Semantic Release 2 | 3 | on: 4 | workflow_run: 5 | workflows: [CI] 6 | types: [completed] 7 | branches: [main] 8 | 9 | jobs: 10 | semantic-release: 11 | name: Semantic Release 12 | runs-on: ubuntu-24.04 13 | # Ensure CI workflow is succeeded and avoid semantic release on forked repository 14 | if: github.event.workflow_run.conclusion == 'success' && github.repository == '{{ repo_namespace }}/{{ repo_name }}' 15 | permissions: 16 | contents: write 17 | id-token: write 18 | issues: write 19 | pull-requests: write 20 | steps: 21 | - id: generate-token 22 | name: Generate a token with GitHub App if App ID exists 23 | if: vars.BOT_APP_ID 24 | uses: actions/create-github-app-token@7e473efe3cb98aa54f8d4bac15400b15fad77d94 # v2.2.0 25 | with: 26 | app-id: {{ '${{ vars.BOT_APP_ID }}' }} 27 | private-key: {{ '${{ secrets.BOT_PRIVATE_KEY }}' }} 28 | - name: Warn if use GITHUB_TOKEN 29 | run: | 30 | if [ -z "{{ '${{ steps.generate-token.outputs.token || secrets.PAT }}' }}" ]; then 31 | echo "# :warning: GITHUB_TOKEN is used for semantic-release" >> $GITHUB_STEP_SUMMARY 32 | echo "The GITHUB_TOKEN is used instead of a bot token or PAT and will not emit the released publish event for the released workflow." >> $GITHUB_STEP_SUMMARY 33 | fi 34 | - name: Checkout repository 35 | uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 36 | with: 37 | fetch-depth: 0 38 | persist-credentials: false 39 | - name: Setup Node.js 40 | uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6.0.0 41 | with: 42 | node-version: 'lts/*' 43 | - name: Semantic Release 44 | env: 45 | GITHUB_TOKEN: {{ '${{ steps.generate-token.outputs.token || secrets.PAT || secrets.GITHUB_TOKEN }}' }} 46 | run: > 47 | npx 48 | --package conventional-changelog-conventionalcommits@9.1.0 49 | --package semantic-release@25.0.2 50 | semantic-release 51 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "[jsonc]": { 3 | "editor.defaultFormatter": "vscode.json-language-features" 4 | }, 5 | "[markdown]": { 6 | "editor.defaultFormatter": "DavidAnson.vscode-markdownlint" 7 | }, 8 | "[python]": { 9 | "editor.codeActionsOnSave": { 10 | "source.fixAll.ruff": "explicit", 11 | "source.organizeImports.ruff": "explicit" 12 | }, 13 | "editor.defaultFormatter": "charliermarsh.ruff", 14 | "editor.formatOnSave": true 15 | }, 16 | "cSpell.words": [ 17 | "autofix", 18 | "automodule", 19 | "cobertura", 20 | "codespell", 21 | "commitlint", 22 | "conventionalcommits", 23 | "datasource", 24 | "deepclean", 25 | "deflist", 26 | "devcontainer", 27 | "devcontainers", 28 | "elif", 29 | "endmacro", 30 | "epub", 31 | "furo", 32 | "genindex", 33 | "huxuan", 34 | "interruptible", 35 | "JPKXI", 36 | "maxdepth", 37 | "modindex", 38 | "mypy", 39 | "noninteractive", 40 | "pathjoin", 41 | "pipenv", 42 | "pipx", 43 | "pycache", 44 | "pydantic", 45 | "pypi", 46 | "pyproject", 47 | "pytest", 48 | "Quickstart", 49 | "renovatebot", 50 | "repology", 51 | "setuptools", 52 | "softprops", 53 | "sphinxcontrib", 54 | "titlesonly", 55 | "toctree", 56 | "unshallow", 57 | "viewcode" 58 | ], 59 | "editor.codeActionsOnSave": { 60 | "source.fixAll": "explicit" 61 | }, 62 | "editor.formatOnSave": true, 63 | "editor.rulers": [ 64 | 88 65 | ], 66 | "files.exclude": { 67 | "**/*.egg-info": true, 68 | "**/.coverage": true, 69 | "**/.mypy_cache": true, 70 | "**/.pdm-build": true, 71 | "**/.pytest_cache": true, 72 | "**/.ruff_cache": true, 73 | "**/.venv": true, 74 | "**/Pipfile*": true, 75 | "**/__pycache__": true, 76 | "**/_build": true, 77 | "**/coverage.xml": true, 78 | "**/htmlcov": true 79 | }, 80 | "files.insertFinalNewline": true, 81 | "files.trimFinalNewlines": true, 82 | "files.trimTrailingWhitespace": true, 83 | "myst.preview.extensions": [ 84 | "dollarmath", 85 | "deflist" 86 | ], 87 | "sortJSON.contextMenu": { 88 | "sortJSONAlphaNum": false, 89 | "sortJSONAlphaNumReverse": false, 90 | "sortJSONKeyLength": false, 91 | "sortJSONKeyLengthReverse": false, 92 | "sortJSONReverse": false, 93 | "sortJSONType": false, 94 | "sortJSONTypeReverse": false, 95 | "sortJSONValues": false, 96 | "sortJSONValuesReverse": false 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /template/.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "[jsonc]": { 3 | "editor.defaultFormatter": "vscode.json-language-features" 4 | }, 5 | "[markdown]": { 6 | "editor.defaultFormatter": "DavidAnson.vscode-markdownlint" 7 | }, 8 | "[python]": { 9 | "editor.codeActionsOnSave": { 10 | "source.fixAll.ruff": "explicit", 11 | "source.organizeImports.ruff": "explicit" 12 | }, 13 | "editor.defaultFormatter": "charliermarsh.ruff", 14 | "editor.formatOnSave": true 15 | }, 16 | "cSpell.words": [ 17 | "autofix", 18 | "automodule", 19 | "cobertura", 20 | "codespell", 21 | "commitlint", 22 | "conventionalcommits", 23 | "datasource", 24 | "deepclean", 25 | "deflist", 26 | "devcontainer", 27 | "devcontainers", 28 | "elif", 29 | "endmacro", 30 | "epub", 31 | "furo", 32 | "genindex", 33 | "huxuan", 34 | "interruptible", 35 | "JPKXI", 36 | "maxdepth", 37 | "modindex", 38 | "mypy", 39 | "noninteractive", 40 | "pathjoin", 41 | "pipenv", 42 | "pipx", 43 | "pycache", 44 | "pydantic", 45 | "pypi", 46 | "pyproject", 47 | "pytest", 48 | "Quickstart", 49 | "renovatebot", 50 | "repology", 51 | "setuptools", 52 | "softprops", 53 | "sphinxcontrib", 54 | "titlesonly", 55 | "toctree", 56 | "unshallow", 57 | "viewcode" 58 | ], 59 | "editor.codeActionsOnSave": { 60 | "source.fixAll": "explicit" 61 | }, 62 | "editor.formatOnSave": true, 63 | "editor.rulers": [ 64 | 88 65 | ], 66 | "files.exclude": { 67 | "**/*.egg-info": true, 68 | "**/.coverage": true, 69 | "**/.mypy_cache": true, 70 | "**/.pdm-build": true, 71 | "**/.pytest_cache": true, 72 | "**/.ruff_cache": true, 73 | "**/.venv": true, 74 | "**/Pipfile*": true, 75 | "**/__pycache__": true, 76 | "**/_build": true, 77 | "**/coverage.xml": true, 78 | "**/htmlcov": true 79 | }, 80 | "files.insertFinalNewline": true, 81 | "files.trimFinalNewlines": true, 82 | "files.trimTrailingWhitespace": true, 83 | "myst.preview.extensions": [ 84 | "dollarmath", 85 | "deflist" 86 | ], 87 | "sortJSON.contextMenu": { 88 | "sortJSONAlphaNum": false, 89 | "sortJSONAlphaNumReverse": false, 90 | "sortJSONKeyLength": false, 91 | "sortJSONKeyLengthReverse": false, 92 | "sortJSONReverse": false, 93 | "sortJSONType": false, 94 | "sortJSONTypeReverse": false, 95 | "sortJSONValues": false, 96 | "sortJSONValuesReverse": false 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /docs/advanced/cicd.md: -------------------------------------------------------------------------------- 1 | # CI/CD Configurations 2 | 3 | The CI/CD (Continuous Integration and Continuous Delivery) workflows automate various development tasks to ensure project maintainability with minimal human effort. The configuration files are located at `.github/workflows/*.yml` for GitHub and `.gitlab/workflows/*.yml` for GitLab. 4 | 5 | ## `ci.yml` 6 | 7 | The `ci` workflow is the most frequently used workflow, running on all pull/merge requests and changes to the default `main` branch. It performs linting, testing, and builds for the documentation and the package across all supported operation systems and Python versions to ensure everything works as expected. 8 | 9 | ## `commitlint.yml` 10 | 11 | The `commitlint` workflow checks whether the pull/merge request title comply with the . This ensures consistent commit history and enable the possibility of automated release pipeline. 12 | 13 | ## `delete-untagged-packages.yml` 14 | 15 | The `delete-untagged-packages` workflow removes untagged packages since GitHub will still keep the package when overridden with the same tag. It helps keep the GitHub Packages clean and tidy. 16 | 17 | ## `devcontainer.yml` 18 | 19 | The `devcontainer` workflow will be triggered by container related changes. It builds and tests the development and production containers and push the development container except during pull/merge requests, ensuring seamless containerized environments. 20 | 21 | ## `readthedocs-preview.yml` 22 | 23 | The `readthedocs-preview` workflow leverage the [readthedocs/actions/preview](https://github.com/readthedocs/actions/tree/v1/preview) to add Read the Docs preview links to the related pull requests. These links make it easy to review documentation changes. 24 | 25 | ## `release.yml` 26 | 27 | The `release` workflow manages the entire publish process, including publishing the documentation, containers and packages. It is triggered by a new release or a release tag. It also ensures all the builds and tests are succeed before completing the release. 28 | 29 | ## `renovate.yml` 30 | 31 | The `renovate` workflow automates the . It is scheduled to run weekly and will create pull/merges request when there are new versions of the scaffold template, Python packages, GitHub Runners, GitHub Actions, docker images and etc. It keeps the project secure and ensures compatibility with the latest versions. 32 | 33 | ## `semantic-release.yml` 34 | 35 | The `semantic-release` workflow automate the versioning and release process by publishing new releases or new release tags when certain changes are pushed to the default `main` branch. It simplifies the release management while maintaining consistency. 36 | -------------------------------------------------------------------------------- /docs/conf.py: -------------------------------------------------------------------------------- 1 | """Configuration file for the Sphinx documentation builder. 2 | 3 | For the full list of built-in configuration values, see the documentation: 4 | https://www.sphinx-doc.org/en/master/usage/configuration.html 5 | """ 6 | 7 | from importlib import metadata 8 | 9 | # -- Project information --------------------------------------------------------------- 10 | # https://www.sphinx-doc.org/en/master/usage/configuration.html#project-information 11 | 12 | author = "huxuan" 13 | copyright = "2022-2025, huxuan" 14 | project = "Serious Scaffold Python" 15 | release = metadata.version("ss-python") 16 | version = ".".join(release.split(".")[:2]) 17 | 18 | 19 | # -- General configuration ------------------------------------------------------------- 20 | # https://www.sphinx-doc.org/en/master/usage/configuration.html#general-configuration 21 | 22 | extensions = [ 23 | "myst_parser", 24 | "sphinx.ext.autodoc", 25 | "sphinx.ext.napoleon", 26 | "sphinx.ext.viewcode", 27 | "sphinx_click", 28 | "sphinx_design", 29 | "sphinxcontrib.autodoc_pydantic", 30 | ] 31 | source_suffix = { 32 | ".rst": "restructuredtext", 33 | ".md": "markdown", 34 | } 35 | exclude_patterns = ["_build", "Thumbs.db", ".DS_Store"] 36 | templates_path = ["_templates"] 37 | html_theme_options = { 38 | "announcement": ( 39 | "Serious Scaffold Python " 40 | "is in the Alpha phase. " 41 | "Frequent changes and instability should be anticipated. " 42 | "Any feedback, comments, suggestions and contributions are welcome!" 43 | ), 44 | } 45 | 46 | # -- Options for HTML output ----------------------------------------------------------- 47 | # https://www.sphinx-doc.org/en/master/usage/configuration.html#options-for-html-output 48 | 49 | html_theme = "furo" 50 | html_static_path = ["_static"] 51 | 52 | # -- Options for autodoc extension ---------------------------------------------------- 53 | # https://www.sphinx-doc.org/en/master/usage/extensions/autodoc.html#configuration 54 | 55 | autodoc_default_options = { 56 | "members": None, 57 | } 58 | 59 | # -- Options for autodoc_pydantic extension ------------------------------------------- 60 | # https://autodoc-pydantic.readthedocs.io/en/stable/users/configuration.html 61 | 62 | autodoc_pydantic_settings_show_json = False 63 | 64 | # -- Options for myst-parser extension ------------------------------------------------ 65 | # https://myst-parser.readthedocs.io/en/latest/configuration.html 66 | 67 | myst_enable_extensions = [ 68 | "colon_fence", 69 | "deflist", 70 | ] 71 | myst_heading_anchors = 3 72 | myst_url_schemes = { 73 | "http": None, 74 | "https": None, 75 | "vscode": None, 76 | } 77 | -------------------------------------------------------------------------------- /.gitlab/workflows/devcontainer.yml: -------------------------------------------------------------------------------- 1 | dev-container-publish: 2 | image: docker:29.1.1@sha256:9b20eb23e1f0443655673efb9db76c4b18cc1b45de1fcf82b3c1b749b9647bdf 3 | parallel: 4 | matrix: 5 | - PYTHON_VERSION: 6 | - '3.9' 7 | - '3.10' 8 | - '3.11' 9 | - '3.12' 10 | - '3.13' 11 | rules: 12 | - changes: 13 | - .devcontainer/Dockerfile 14 | - .devcontainer/Dockerfile.dockerignore 15 | - .gitlab/workflows/devcontainer.yml 16 | - Makefile 17 | if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH && $CI_PIPELINE_SOURCE == "push" 18 | - if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH && $CI_PIPELINE_SOURCE == "web" 19 | - changes: 20 | - .devcontainer/Dockerfile 21 | - .devcontainer/Dockerfile.dockerignore 22 | - .gitlab/workflows/devcontainer.yml 23 | - Makefile 24 | if: $CI_PIPELINE_SOURCE == 'merge_request_event' 25 | script: 26 | - docker login -u ${CI_REGISTRY_USER} -p ${CI_REGISTRY_PASSWORD} ${CI_REGISTRY} 27 | - docker context create builder 28 | - docker buildx create builder --name container --driver docker-container --use 29 | - docker buildx inspect --bootstrap --builder container 30 | - | 31 | docker buildx build . \ 32 | --build-arg PYTHON_VERSION=${PYTHON_VERSION} \ 33 | --cache-from type=registry,ref=${CI_REGISTRY_IMAGE}/dev-cache:py${PYTHON_VERSION} \ 34 | --file .devcontainer/Dockerfile \ 35 | --load \ 36 | --tag ${CI_REGISTRY_IMAGE}/dev:py${PYTHON_VERSION} \ 37 | --target dev 38 | - | 39 | docker run --rm \ 40 | -e CI=true \ 41 | -v ${PWD}:/workspace \ 42 | ${CI_REGISTRY_IMAGE}/dev:py${PYTHON_VERSION} \ 43 | make dev lint test doc build 44 | - | 45 | docker buildx build . \ 46 | --build-arg PYTHON_VERSION=${PYTHON_VERSION} \ 47 | --file .devcontainer/Dockerfile \ 48 | --load \ 49 | --tag ${CI_REGISTRY_IMAGE}:py${PYTHON_VERSION} \ 50 | --target prod 51 | - docker run --rm ${CI_REGISTRY_IMAGE}:py${PYTHON_VERSION} 52 | - | 53 | if [ "$CI_PIPELINE_SOURCE" != "merge_request_event" ]; then 54 | docker buildx build . \ 55 | --build-arg PYTHON_VERSION=${PYTHON_VERSION} \ 56 | --cache-to type=registry,ref=${CI_REGISTRY_IMAGE}/dev-cache:py${PYTHON_VERSION},mode=max \ 57 | --file .devcontainer/Dockerfile \ 58 | --push \ 59 | --tag ${CI_REGISTRY_IMAGE}/dev:py${PYTHON_VERSION} \ 60 | --target dev 61 | fi 62 | services: 63 | - docker:29.1.1@sha256:9b20eb23e1f0443655673efb9db76c4b18cc1b45de1fcf82b3c1b749b9647bdf 64 | stage: build 65 | variables: 66 | DOCKER_TLS_CERTDIR: /certs 67 | PYTHON_VERSION: ${PYTHON_VERSION} 68 | SOURCE_DATE_EPOCH: 0 69 | -------------------------------------------------------------------------------- /template/docs/advanced/cicd.md.jinja: -------------------------------------------------------------------------------- 1 | # CI/CD Configurations 2 | 3 | The CI/CD (Continuous Integration and Continuous Delivery) workflows automate various development tasks to ensure project maintainability with minimal human effort. The configuration files are located at `.github/workflows/*.yml` for GitHub and `.gitlab/workflows/*.yml` for GitLab. 4 | 5 | ## `ci.yml` 6 | 7 | The `ci` workflow is the most frequently used workflow, running on all pull/merge requests and changes to the default `main` branch. It performs linting, testing, and builds for the documentation and the package across all supported operation systems and Python versions to ensure everything works as expected. 8 | 9 | ## `commitlint.yml` 10 | 11 | The `commitlint` workflow checks whether the pull/merge request title comply with the . This ensures consistent commit history and enable the possibility of automated release pipeline. 12 | 13 | {% if repo_platform == 'github' %} 14 | ## `delete-untagged-packages.yml` 15 | 16 | The `delete-untagged-packages` workflow removes untagged packages since GitHub will still keep the package when overridden with the same tag. It helps keep the GitHub Packages clean and tidy. 17 | {% endif %} 18 | 19 | ## `devcontainer.yml` 20 | 21 | The `devcontainer` workflow will be triggered by container related changes. It builds and tests the development and production containers and push the development container except during pull/merge requests, ensuring seamless containerized environments. 22 | 23 | {% if repo_platform == 'github' %} 24 | ## `readthedocs-preview.yml` 25 | 26 | The `readthedocs-preview` workflow leverage the [readthedocs/actions/preview](https://github.com/readthedocs/actions/tree/v1/preview) to add Read the Docs preview links to the related pull requests. These links make it easy to review documentation changes. 27 | {% endif %} 28 | 29 | ## `release.yml` 30 | 31 | The `release` workflow manages the entire publish process, including publishing the documentation, containers and packages. It is triggered by a new release or a release tag. It also ensures all the builds and tests are succeed before completing the release. 32 | 33 | ## `renovate.yml` 34 | 35 | The `renovate` workflow automates the . It is scheduled to run weekly and will create pull/merges request when there are new versions of the scaffold template, Python packages, GitHub Runners, GitHub Actions, docker images and etc. It keeps the project secure and ensures compatibility with the latest versions. 36 | 37 | ## `semantic-release.yml` 38 | 39 | The `semantic-release` workflow automate the versioning and release process by publishing new releases or new release tags when certain changes are pushed to the default `main` branch. It simplifies the release management while maintaining consistency. 40 | -------------------------------------------------------------------------------- /template/{% if repo_platform == 'github' %}.github{% endif %}/workflows/ci.yml.jinja: -------------------------------------------------------------------------------- 1 | {% from pathjoin("includes", "version_compare.jinja") import version_between %} 2 | name: CI 3 | 4 | on: 5 | pull_request: 6 | push: 7 | branches: 8 | - main 9 | 10 | permissions: 11 | contents: read 12 | 13 | concurrency: 14 | cancel-in-progress: true 15 | group: {{ '${{ github.workflow }}-${{ github.ref }}' }} 16 | 17 | jobs: 18 | ci: 19 | if: {{ '${{ !cancelled() && ! failure() }}' }} 20 | runs-on: {{ '${{ matrix.os }}' }} 21 | steps: 22 | - name: Checkout repository 23 | uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 24 | with: 25 | fetch-depth: 0 26 | - name: Set up PDM 27 | uses: pdm-project/setup-pdm@94a823180e06fcde4ad29308721954a521c96ed0 # v4.4 28 | with: 29 | cache: true 30 | python-version: {{ '${{ matrix.python-version }}' }} 31 | version: 2.25.4 32 | cache-dependency-path: | 33 | ./pdm.dev.lock 34 | ./pdm.lock 35 | - run: env | sort 36 | - run: make prerequisites 37 | - run: make dev 38 | - run: make lint test doc build 39 | strategy: 40 | matrix: 41 | os: 42 | {% if "macos" in platforms %} 43 | # renovate: github-runner 44 | - macos-14 45 | {% endif %} 46 | {% if "linux" in platforms %} 47 | # renovate: github-runner 48 | - ubuntu-24.04 49 | {% endif %} 50 | {% if "windows" in platforms %} 51 | # renovate: github-runner 52 | - windows-2025 53 | {% endif %} 54 | python-version: 55 | {% if version_between("3.9", min_py, max_py) %} 56 | - "3.9" 57 | {% endif %} 58 | {% if version_between("3.10", min_py, max_py) %} 59 | - "3.10" 60 | {% endif %} 61 | {% if version_between("3.11", min_py, max_py) %} 62 | - "3.11" 63 | {% endif %} 64 | {% if version_between("3.12", min_py, max_py) %} 65 | - "3.12" 66 | {% endif %} 67 | {% if version_between("3.13", min_py, max_py) %} 68 | - "3.13" 69 | {% endif %} 70 | {% if project_name == "Serious Scaffold Python" %} 71 | consistency: 72 | if: {{ '${{ !cancelled() && ! failure() }}' }} 73 | runs-on: ubuntu-24.04 74 | steps: 75 | - run: env | sort 76 | - name: Checkout repository 77 | uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 78 | - name: Set up Git 79 | run: | 80 | git config --global user.name github-actions 81 | git config --global user.email github-actions@github.com 82 | - run: pipx install copier==9.8.0 83 | - run: make template-build 84 | - run: git diff 85 | - run: git status --porcelain 86 | - run: test -z "$(git status --porcelain)" 87 | {% endif %} 88 | -------------------------------------------------------------------------------- /docs/development/commit.md: -------------------------------------------------------------------------------- 1 | # Commit Convention 2 | 3 | Using structured commit messages, we can enhance the readability of our project history, simplify automated changelog generation, and streamline the release process. We primarily follow the [Conventional Commit](https://www.conventionalcommits.org/) and [Angular's commit guidelines](https://github.com/angular/angular.js/blob/master/DEVELOPERS.md#commits). 4 | 5 | ## Commit Message Pattern 6 | 7 | ```text 8 | (): 9 | ``` 10 | 11 | Examples: 12 | 13 | ```text 14 | build(dependencies): bump the prod group with 9 updates. 15 | doc: Add doc for commit convention. 16 | chore: remove deprecated key in ruff config. 17 | ``` 18 | 19 | Type 20 | : Describes the nature of the change: 21 | 22 | | Type | Description | 23 | |-----------|--------------------------------------------------------| 24 | | `build` | Changes that affect the build system or dependencies. | 25 | | `chore` | Routine tasks or changes outside the src/runtime code. | 26 | | `ci` | Changes related to continuous integration. | 27 | | `doc` | Documentation changes. | 28 | | `feat` | New features. | 29 | | `fix` | Bug fixes. | 30 | | `perf` | Performance improvements. | 31 | | `refactor`| Code restructuring without changing behavior. | 32 | | `revert` | Revert a previous commit. | 33 | | `style` | Code formatting changes. | 34 | | `test` | Add or update tests. | 35 | 36 | Scope [Optional] 37 | : Represents the part of the project impacted by the change. Examples include `logging`, `settings`, and `cli`. 38 | 39 | ### Breaking Change 40 | 41 | A "breaking change" refers to any modification that disrupts the existing functionality in a way that may affect users. It can be denoted using an exclamation mark (`!`) before the colon, like `refactor!: Stuff`. 42 | 43 | ## Commit in Development Branches 44 | 45 | While the commit convention seems strict, we aim for flexibility during the development phase. 46 | By adhering to the , all changes should be introduced via pull/merge requests. 47 | Using the squash merge strategy, the emphasis is primarily on the title of pull/merge requests. 48 | In this way, individual commit within development branches does not need to strictly adhere to the commit convention. 49 | 50 | ````{note} 51 | A CI/CD pipeline checks the titles of pull/merge requests against the following regex pattern: 52 | 53 | ```text 54 | ^(build|chore|ci|doc|feat|fix|perf|refactor|revert|style|test)(\(\w+\))?!?:\s.* 55 | ``` 56 | ```` 57 | -------------------------------------------------------------------------------- /template/docs/development/commit.md: -------------------------------------------------------------------------------- 1 | # Commit Convention 2 | 3 | Using structured commit messages, we can enhance the readability of our project history, simplify automated changelog generation, and streamline the release process. We primarily follow the [Conventional Commit](https://www.conventionalcommits.org/) and [Angular's commit guidelines](https://github.com/angular/angular.js/blob/master/DEVELOPERS.md#commits). 4 | 5 | ## Commit Message Pattern 6 | 7 | ```text 8 | (): 9 | ``` 10 | 11 | Examples: 12 | 13 | ```text 14 | build(dependencies): bump the prod group with 9 updates. 15 | doc: Add doc for commit convention. 16 | chore: remove deprecated key in ruff config. 17 | ``` 18 | 19 | Type 20 | : Describes the nature of the change: 21 | 22 | | Type | Description | 23 | |-----------|--------------------------------------------------------| 24 | | `build` | Changes that affect the build system or dependencies. | 25 | | `chore` | Routine tasks or changes outside the src/runtime code. | 26 | | `ci` | Changes related to continuous integration. | 27 | | `doc` | Documentation changes. | 28 | | `feat` | New features. | 29 | | `fix` | Bug fixes. | 30 | | `perf` | Performance improvements. | 31 | | `refactor`| Code restructuring without changing behavior. | 32 | | `revert` | Revert a previous commit. | 33 | | `style` | Code formatting changes. | 34 | | `test` | Add or update tests. | 35 | 36 | Scope [Optional] 37 | : Represents the part of the project impacted by the change. Examples include `logging`, `settings`, and `cli`. 38 | 39 | ### Breaking Change 40 | 41 | A "breaking change" refers to any modification that disrupts the existing functionality in a way that may affect users. It can be denoted using an exclamation mark (`!`) before the colon, like `refactor!: Stuff`. 42 | 43 | ## Commit in Development Branches 44 | 45 | While the commit convention seems strict, we aim for flexibility during the development phase. 46 | By adhering to the , all changes should be introduced via pull/merge requests. 47 | Using the squash merge strategy, the emphasis is primarily on the title of pull/merge requests. 48 | In this way, individual commit within development branches does not need to strictly adhere to the commit convention. 49 | 50 | ````{note} 51 | A CI/CD pipeline checks the titles of pull/merge requests against the following regex pattern: 52 | 53 | ```text 54 | ^(build|chore|ci|doc|feat|fix|perf|refactor|revert|style|test)(\(\w+\))?!?:\s.* 55 | ``` 56 | ```` 57 | -------------------------------------------------------------------------------- /template/docs/conf.py.jinja: -------------------------------------------------------------------------------- 1 | """Configuration file for the Sphinx documentation builder. 2 | 3 | For the full list of built-in configuration values, see the documentation: 4 | https://www.sphinx-doc.org/en/master/usage/configuration.html 5 | """ 6 | 7 | from importlib import metadata 8 | 9 | # -- Project information --------------------------------------------------------------- 10 | # https://www.sphinx-doc.org/en/master/usage/configuration.html#project-information 11 | 12 | author = "{{ author_name }}" 13 | copyright = "{{ copyright_year }}, {{ author_name }}" 14 | project = "{{ project_name }}" 15 | release = metadata.version("{{ package_name }}") 16 | version = ".".join(release.split(".")[:2]) 17 | 18 | 19 | # -- General configuration ------------------------------------------------------------- 20 | # https://www.sphinx-doc.org/en/master/usage/configuration.html#general-configuration 21 | 22 | extensions = [ 23 | "myst_parser", 24 | "sphinx.ext.autodoc", 25 | "sphinx.ext.napoleon", 26 | "sphinx.ext.viewcode", 27 | "sphinx_click", 28 | "sphinx_design", 29 | "sphinxcontrib.autodoc_pydantic", 30 | ] 31 | source_suffix = { 32 | ".rst": "restructuredtext", 33 | ".md": "markdown", 34 | } 35 | exclude_patterns = ["_build", "Thumbs.db", ".DS_Store"] 36 | templates_path = ["_templates"] 37 | html_theme_options = { 38 | "announcement": ( 39 | "{{ project_name }} " 40 | "is in the {{ development_status }} phase. " 41 | {% if development_status == "Alpha" %} 42 | "Frequent changes and instability should be anticipated. " 43 | {% elif development_status == "Beta" %} 44 | "Changes and potential instability should be anticipated. " 45 | {% endif %} 46 | "Any feedback, comments, suggestions and contributions are welcome!" 47 | ), 48 | } 49 | 50 | # -- Options for HTML output ----------------------------------------------------------- 51 | # https://www.sphinx-doc.org/en/master/usage/configuration.html#options-for-html-output 52 | 53 | html_theme = "furo" 54 | html_static_path = ["_static"] 55 | 56 | # -- Options for autodoc extension ---------------------------------------------------- 57 | # https://www.sphinx-doc.org/en/master/usage/extensions/autodoc.html#configuration 58 | 59 | autodoc_default_options = { 60 | "members": None, 61 | } 62 | 63 | # -- Options for autodoc_pydantic extension ------------------------------------------- 64 | # https://autodoc-pydantic.readthedocs.io/en/stable/users/configuration.html 65 | 66 | autodoc_pydantic_settings_show_json = False 67 | 68 | # -- Options for myst-parser extension ------------------------------------------------ 69 | # https://myst-parser.readthedocs.io/en/latest/configuration.html 70 | 71 | myst_enable_extensions = [ 72 | "colon_fence", 73 | "deflist", 74 | ] 75 | myst_heading_anchors = 3 76 | myst_url_schemes = { 77 | "http": None, 78 | "https": None, 79 | "vscode": None, 80 | } 81 | -------------------------------------------------------------------------------- /.github/workflows/renovate.yml: -------------------------------------------------------------------------------- 1 | name: Renovate 2 | 3 | on: 4 | schedule: 5 | # * is a special character in YAML so you have to quote this string 6 | - cron: "*/15 0-3 * * 1" 7 | workflow_dispatch: null 8 | 9 | permissions: 10 | contents: read 11 | issues: write 12 | pull-requests: write 13 | 14 | jobs: 15 | renovate: 16 | container: 17 | env: 18 | LOG_LEVEL: debug 19 | RENOVATE_ALLOWED_POST_UPGRADE_COMMANDS: '["^git", "^pdm", "^pip", "^copier", "^find"]' 20 | RENOVATE_BRANCH_PREFIX: renovate-github/ 21 | RENOVATE_ENABLED: ${{ vars.RENOVATE_ENABLED || true }} 22 | RENOVATE_ENABLED_MANAGERS: '["pep621", "github-actions", "gitlabci", "regex", "pre-commit"]' 23 | RENOVATE_OPTIMIZE_FOR_DISABLED: "true" 24 | RENOVATE_PLATFORM: github 25 | RENOVATE_REPOSITORIES: '["${{ github.repository }}"]' 26 | RENOVATE_REPOSITORY_CACHE: enabled 27 | image: ghcr.io/renovatebot/renovate:42.30.2@sha256:582aa8b54c8f094fa57249ce5f143ee52f2e6f56c0952dc9a16e8dd3cd37586b 28 | options: "--user root" 29 | runs-on: ubuntu-24.04 30 | steps: 31 | - run: env | sort 32 | - id: generate-token 33 | name: Generate a token with GitHub App if App ID exists 34 | if: vars.BOT_APP_ID 35 | uses: actions/create-github-app-token@7e473efe3cb98aa54f8d4bac15400b15fad77d94 # v2.2.0 36 | with: 37 | app-id: ${{ vars.BOT_APP_ID }} 38 | private-key: ${{ secrets.BOT_PRIVATE_KEY }} 39 | - name: Warn if use GITHUB_TOKEN 40 | run: | 41 | if [ -z "${{ steps.generate-token.outputs.token || secrets.PAT }}" ]; then 42 | echo "# :warning: GITHUB_TOKEN is used for renovate" >> $GITHUB_STEP_SUMMARY 43 | echo "The GITHUB_TOKEN is used instead of a bot token or PAT and will not emit the checks for the pull requests." >> $GITHUB_STEP_SUMMARY 44 | fi 45 | - name: Warn if RENOVATE_GIT_AUTHOR is set while using GitHub App token 46 | if: steps.generate-token.outputs.token && vars.RENOVATE_GIT_AUTHOR 47 | run: | 48 | echo "# :warning: `RENOVATE_GIT_AUTHOR` is set explicitly while using GitHub App token" >> $GITHUB_STEP_SUMMARY 49 | echo "Generally, Renovate automatically detects the git author and email using the token. However, explicitly setting the `RENOVATE_GIT_AUTHOR` will override this behavior." >> $GITHUB_STEP_SUMMARY 50 | - name: Run Renovate 51 | env: 52 | RENOVATE_GIT_AUTHOR: ${{ vars.RENOVATE_GIT_AUTHOR }} 53 | RENOVATE_PLATFORM_COMMIT: ${{ steps.generate-token.outputs.token && true || false }} 54 | RENOVATE_TOKEN: ${{ steps.generate-token.outputs.token || secrets.PAT || secrets.GITHUB_TOKEN }} 55 | run: | 56 | if [ -z "$RENOVATE_TOKEN" ]; then 57 | echo "RENOVATE_TOKEN is not properly configured, skipping ..." 58 | else 59 | renovate $RENOVATE_EXTRA_FLAG 60 | fi 61 | -------------------------------------------------------------------------------- /template/README.md.jinja: -------------------------------------------------------------------------------- 1 | {% from pathjoin("includes", "variable.jinja") import coverage_badge with context %} 2 | {% from pathjoin("includes", "variable.jinja") import license_badge with context %} 3 | {% from pathjoin("includes", "variable.jinja") import license_url with context %} 4 | {% from pathjoin("includes", "variable.jinja") import logo_badge with context %} 5 | {% from pathjoin("includes", "variable.jinja") import logo_badge_url with context %} 6 | {% from pathjoin("includes", "variable.jinja") import page_url with context %} 7 | {% from pathjoin("includes", "variable.jinja") import pipeline_badge with context %} 8 | {% from pathjoin("includes", "variable.jinja") import release_badge with context %} 9 | {% from pathjoin("includes", "variable.jinja") import repo_url with context %} 10 | # {{ project_name }} 11 | 12 | {{ project_description }} 13 | 14 | {{ pipeline_badge() }} 15 | {{ coverage_badge() }} 16 | {{ release_badge() }} 17 | [![PyPI - Python Version](https://img.shields.io/pypi/pyversions/{{ package_name }})](https://pypi.org/project/{{ package_name }}/) 18 | {{ license_badge() }} 19 | 20 | [![pdm-managed](https://img.shields.io/badge/pdm-managed-blueviolet)](https://pdm-project.org) 21 | [![pre-commit](https://img.shields.io/badge/pre--commit-enabled-brightgreen?logo=pre-commit)](https://github.com/pre-commit/pre-commit) 22 | [![Checked with mypy](https://www.mypy-lang.org/static/mypy_badge.svg)](http://mypy-lang.org/) 23 | [![Ruff](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/ruff/main/assets/badge/v2.json)](https://github.com/astral-sh/ruff) 24 | [![Conventional Commits](https://img.shields.io/badge/Conventional%20Commits-1.0.0-%23FE5196?logo=conventionalcommits&logoColor=white)](https://conventionalcommits.org) 25 | [![Pydantic v2](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/pydantic/pydantic/5697b1e4c4a9790ece607654e6c02a160620c7e1/docs/badge/v2.json)](https://pydantic.dev) 26 | [![Copier](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/copier-org/copier/master/img/badge/badge-grayscale-inverted-border-orange.json)](https://github.com/copier-org/copier) 27 | {{ logo_badge() }} 28 | [![Open in Dev Containers](https://img.shields.io/static/v1?label=Dev%20Containers&message=Open&color=blue&logo=visualstudiocode)](https://vscode.dev/redirect?url=vscode://ms-vscode-remote.remote-containers/cloneInVolume?url=https://{{ repo_url() }}) 29 | 30 | {% if development_status == "Alpha" %} 31 | > [!WARNING] 32 | {% elif development_status == "Beta" %} 33 | > [!IMPORTANT] 34 | {% elif development_status == "Stable" %} 35 | > [!NOTE] 36 | {% endif %} 37 | > _{{ project_name }}_ is in the **{{ development_status }}** phase. 38 | {% if development_status == "Alpha" %} 39 | > Frequent changes and instability should be anticipated. 40 | {% elif development_status == "Beta" %} 41 | > Changes and potential instability should be anticipated. 42 | {% endif %} 43 | > Any feedback, comments, suggestions and contributions are welcome! 44 | 45 | {{ readme_content }} 46 | ## 📜 License 47 | 48 | {{ copyright_license }}, for more details, see the [LICENSE]({{ license_url() }}) file. 49 | -------------------------------------------------------------------------------- /template/{% if repo_platform == 'github' %}.github{% endif %}/workflows/renovate.yml.jinja: -------------------------------------------------------------------------------- 1 | name: Renovate 2 | 3 | on: 4 | schedule: 5 | # * is a special character in YAML so you have to quote this string 6 | - cron: "*/15 0-3 * * 1" 7 | workflow_dispatch: null 8 | 9 | permissions: 10 | contents: read 11 | issues: write 12 | pull-requests: write 13 | 14 | jobs: 15 | renovate: 16 | container: 17 | env: 18 | LOG_LEVEL: debug 19 | RENOVATE_ALLOWED_POST_UPGRADE_COMMANDS: '["^git", "^pdm", "^pip", "^copier", "^find"]' 20 | RENOVATE_BRANCH_PREFIX: renovate-github/ 21 | RENOVATE_ENABLED: {{ '${{ vars.RENOVATE_ENABLED || true }}' }} 22 | {% if project_name == "Serious Scaffold Python" %} 23 | RENOVATE_ENABLED_MANAGERS: '["pep621", "github-actions", "gitlabci", "regex", "pre-commit"]' 24 | {% else %} 25 | RENOVATE_ENABLED_MANAGERS: '["pep621", "github-actions", "regex", "pre-commit"]' 26 | {% endif %} 27 | RENOVATE_OPTIMIZE_FOR_DISABLED: "true" 28 | RENOVATE_PLATFORM: github 29 | RENOVATE_REPOSITORIES: '["{{ '${{ github.repository }}' }}"]' 30 | RENOVATE_REPOSITORY_CACHE: enabled 31 | image: ghcr.io/renovatebot/renovate:42.30.2@sha256:582aa8b54c8f094fa57249ce5f143ee52f2e6f56c0952dc9a16e8dd3cd37586b 32 | options: "--user root" 33 | runs-on: ubuntu-24.04 34 | steps: 35 | - run: env | sort 36 | - id: generate-token 37 | name: Generate a token with GitHub App if App ID exists 38 | if: vars.BOT_APP_ID 39 | uses: actions/create-github-app-token@7e473efe3cb98aa54f8d4bac15400b15fad77d94 # v2.2.0 40 | with: 41 | app-id: {{ '${{ vars.BOT_APP_ID }}' }} 42 | private-key: {{ '${{ secrets.BOT_PRIVATE_KEY }}' }} 43 | - name: Warn if use GITHUB_TOKEN 44 | run: | 45 | if [ -z "{{ '${{ steps.generate-token.outputs.token || secrets.PAT }}' }}" ]; then 46 | echo "# :warning: GITHUB_TOKEN is used for renovate" >> $GITHUB_STEP_SUMMARY 47 | echo "The GITHUB_TOKEN is used instead of a bot token or PAT and will not emit the checks for the pull requests." >> $GITHUB_STEP_SUMMARY 48 | fi 49 | - name: Warn if RENOVATE_GIT_AUTHOR is set while using GitHub App token 50 | if: steps.generate-token.outputs.token && vars.RENOVATE_GIT_AUTHOR 51 | run: | 52 | echo "# :warning: `RENOVATE_GIT_AUTHOR` is set explicitly while using GitHub App token" >> $GITHUB_STEP_SUMMARY 53 | echo "Generally, Renovate automatically detects the git author and email using the token. However, explicitly setting the `RENOVATE_GIT_AUTHOR` will override this behavior." >> $GITHUB_STEP_SUMMARY 54 | - name: Run Renovate 55 | env: 56 | RENOVATE_GIT_AUTHOR: {{ '${{ vars.RENOVATE_GIT_AUTHOR }}' }} 57 | RENOVATE_PLATFORM_COMMIT: {{ '${{ steps.generate-token.outputs.token && true || false }}' }} 58 | RENOVATE_TOKEN: {{ '${{ steps.generate-token.outputs.token || secrets.PAT || secrets.GITHUB_TOKEN }}' }} 59 | run: | 60 | if [ -z "$RENOVATE_TOKEN" ]; then 61 | echo "RENOVATE_TOKEN is not properly configured, skipping ..." 62 | else 63 | renovate $RENOVATE_EXTRA_FLAG 64 | fi 65 | -------------------------------------------------------------------------------- /docs/advanced/dev-containers.md: -------------------------------------------------------------------------------- 1 | # Development Container 2 | 3 | Instead of manually configuring your development environment, [Dev Containers](https://containers.dev/) offer a seamless containerized development experience right out of the box. 4 | 5 | ## Prerequisites 6 | 7 | Before you can use a Dev Container, you will need to install a few components. 8 | 9 | 1. [Docker Desktop](https://www.docker.com/products/docker-desktop) or an [alternative Docker option](https://code.visualstudio.com/remote/advancedcontainers/docker-options). 10 | 1. [Visual Studio Code](https://code.visualstudio.com/). 11 | 1. The [Dev Containers extension](vscode:extension/ms-vscode-remote.remote-containers) within VSCode. 12 | 13 | ## Usage 14 | 15 | After installing the prerequisites, you have two main approaches to use a Dev Container. Using [a locally cloned repository](#open-a-locally-cloned-repository-in-a-container) leverages your existing local source code, while [an isolated container volume](#open-the-repository-in-an-isolated-container-volume) creates a separate copy of the repository, which is particularly useful for PR reviews or exploring branches without altering your local environment. 16 | 17 | ### Open a locally cloned repository in a container 18 | 19 | When you open a repository that includes a Dev Container configuration in VS Code, you will receive a prompt to reopen it in the container. 20 | 21 | ```{image} /_static/images/dev-container-reopen-prompt.png 22 | :alt: Dev Container Reopen Prompt. 23 | ``` 24 | 25 | If you missed the prompt, you can use the **Dev Containers: Reopen in Container** command from the Command Palette to initiate the containerized environment. Here are some frequently used commands: 26 | 27 | Dev Containers: Reopen in Container 28 | : Triggers the containerized environment setup upon opening a repository configured for Dev Containers. 29 | 30 | Dev Containers: Rebuild Without Cache and Reopen in Container 31 | : Useful for refreshing your environment in case of issues or to update to a newer version. 32 | 33 | Dev Containers: Clean Up Dev Containers... 34 | : Deletes stopped Dev Container instances and removes unused volumes, helping maintain a clean development environment. 35 | 36 | ### Open the repository in an isolated container volume 37 | 38 | You may already notice the badge [![Open in Dev Containers](https://img.shields.io/static/v1?label=Dev%20Containers&message=Open&color=blue&logo=visualstudiocode)](https://vscode.dev/redirect?url=vscode://ms-vscode-remote.remote-containers/cloneInVolume?url=https://github.com/serious-scaffold/ss-python) in the [Overview](/index.md) page. You can click the badge or [this link](https://vscode.dev/redirect?url=vscode://ms-vscode-remote.remote-containers/cloneInVolume?url=https://github.com/serious-scaffold/ss-python) to get started. Clicking these links will cause VS Code to automatically install the Dev Containers extension if needed, clone the source code into a container volume, and spin up a dev container for use. 39 | 40 | ## Reference 41 | 42 | For more detailed guidance and advanced usage, explore the following resources: 43 | 44 | - [Dev Containers tutorial](https://code.visualstudio.com/docs/devcontainers/tutorial) 45 | - [Developing inside a Container](https://code.visualstudio.com/docs/devcontainers/containers) 46 | -------------------------------------------------------------------------------- /.github/workflows/devcontainer.yml: -------------------------------------------------------------------------------- 1 | name: DevContainer 2 | 3 | on: 4 | pull_request: 5 | paths: 6 | - .devcontainer/Dockerfile 7 | - .devcontainer/Dockerfile.dockerignore 8 | - .github/workflows/devcontainer.yml 9 | - Makefile 10 | push: 11 | branches: 12 | - main 13 | paths: 14 | - .devcontainer/Dockerfile 15 | - .devcontainer/Dockerfile.dockerignore 16 | - .github/workflows/devcontainer.yml 17 | - Makefile 18 | workflow_dispatch: null 19 | 20 | concurrency: 21 | cancel-in-progress: true 22 | group: ${{ github.workflow }}-${{ github.ref }} 23 | 24 | jobs: 25 | dev-container-publish: 26 | permissions: 27 | packages: write 28 | runs-on: ubuntu-24.04 29 | steps: 30 | - run: env | sort 31 | - name: Checkout repository 32 | uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 33 | - name: Set up authentication 34 | run: docker login -u ${{ github.actor }} -p ${{ secrets.GITHUB_TOKEN }} ghcr.io 35 | - name: Set up BuildKit 36 | run: | 37 | docker context create builder 38 | docker buildx create builder --name container --driver docker-container --use 39 | docker buildx inspect --bootstrap --builder container 40 | - name: Build the dev container 41 | run: | 42 | docker buildx build . \ 43 | --build-arg PYTHON_VERSION=${{ matrix.python-version }} \ 44 | --cache-from type=registry,ref=ghcr.io/${{ github.repository }}/dev-cache:py${{ matrix.python-version }} \ 45 | --file .devcontainer/Dockerfile \ 46 | --load \ 47 | --tag ghcr.io/${{ github.repository }}/dev:py${{ matrix.python-version }} \ 48 | --target dev 49 | - name: Test the dev container 50 | run: | 51 | docker run --rm \ 52 | -e CI=true \ 53 | -v ${PWD}:/workspace \ 54 | ghcr.io/${{ github.repository }}/dev:py${{ matrix.python-version }} \ 55 | make dev lint test doc build 56 | - name: Build the prod container 57 | run: | 58 | docker buildx build . \ 59 | --build-arg PYTHON_VERSION=${{ matrix.python-version }} \ 60 | --file .devcontainer/Dockerfile \ 61 | --load \ 62 | --tag ghcr.io/${{ github.repository }}:py${{ matrix.python-version }} \ 63 | --target prod 64 | - name: Test the prod container 65 | run: docker run --rm ghcr.io/${{ github.repository }}:py${{ matrix.python-version }} 66 | - name: Push the dev container 67 | if: github.event_name != 'pull_request' 68 | run: | 69 | docker buildx build . \ 70 | --build-arg PYTHON_VERSION=${{ matrix.python-version }} \ 71 | --cache-to type=registry,ref=ghcr.io/${{ github.repository }}/dev-cache:py${{ matrix.python-version }},mode=max \ 72 | --file .devcontainer/Dockerfile \ 73 | --push \ 74 | --tag ghcr.io/${{ github.repository }}/dev:py${{ matrix.python-version }} \ 75 | --target dev 76 | strategy: 77 | matrix: 78 | python-version: 79 | - '3.9' 80 | - '3.10' 81 | - '3.11' 82 | - '3.12' 83 | - '3.13' 84 | -------------------------------------------------------------------------------- /template/docs/advanced/dev-containers.md.jinja: -------------------------------------------------------------------------------- 1 | # Development Container 2 | 3 | Instead of manually configuring your development environment, [Dev Containers](https://containers.dev/) offer a seamless containerized development experience right out of the box. 4 | 5 | ## Prerequisites 6 | 7 | Before you can use a Dev Container, you will need to install a few components. 8 | 9 | 1. [Docker Desktop](https://www.docker.com/products/docker-desktop) or an [alternative Docker option](https://code.visualstudio.com/remote/advancedcontainers/docker-options). 10 | 1. [Visual Studio Code](https://code.visualstudio.com/). 11 | 1. The [Dev Containers extension](vscode:extension/ms-vscode-remote.remote-containers) within VSCode. 12 | 13 | ## Usage 14 | 15 | After installing the prerequisites, you have two main approaches to use a Dev Container. Using [a locally cloned repository](#open-a-locally-cloned-repository-in-a-container) leverages your existing local source code, while [an isolated container volume](#open-the-repository-in-an-isolated-container-volume) creates a separate copy of the repository, which is particularly useful for PR reviews or exploring branches without altering your local environment. 16 | 17 | ### Open a locally cloned repository in a container 18 | 19 | When you open a repository that includes a Dev Container configuration in VS Code, you will receive a prompt to reopen it in the container. 20 | 21 | ```{image} /_static/images/dev-container-reopen-prompt.png 22 | :alt: Dev Container Reopen Prompt. 23 | ``` 24 | 25 | If you missed the prompt, you can use the **Dev Containers: Reopen in Container** command from the Command Palette to initiate the containerized environment. Here are some frequently used commands: 26 | 27 | Dev Containers: Reopen in Container 28 | : Triggers the containerized environment setup upon opening a repository configured for Dev Containers. 29 | 30 | Dev Containers: Rebuild Without Cache and Reopen in Container 31 | : Useful for refreshing your environment in case of issues or to update to a newer version. 32 | 33 | Dev Containers: Clean Up Dev Containers... 34 | : Deletes stopped Dev Container instances and removes unused volumes, helping maintain a clean development environment. 35 | 36 | ### Open the repository in an isolated container volume 37 | 38 | You may already notice the badge [![Open in Dev Containers](https://img.shields.io/static/v1?label=Dev%20Containers&message=Open&color=blue&logo=visualstudiocode)](https://vscode.dev/redirect?url=vscode://ms-vscode-remote.remote-containers/cloneInVolume?url=https://github.com/{{ repo_namespace }}/{{ repo_name }}) in the [Overview](/index.md) page. You can click the badge or [this link](https://vscode.dev/redirect?url=vscode://ms-vscode-remote.remote-containers/cloneInVolume?url=https://github.com/{{ repo_namespace }}/{{ repo_name }}) to get started. Clicking these links will cause VS Code to automatically install the Dev Containers extension if needed, clone the source code into a container volume, and spin up a dev container for use. 39 | 40 | ## Reference 41 | 42 | For more detailed guidance and advanced usage, explore the following resources: 43 | 44 | - [Dev Containers tutorial](https://code.visualstudio.com/docs/devcontainers/tutorial) 45 | - [Developing inside a Container](https://code.visualstudio.com/docs/devcontainers/containers) 46 | -------------------------------------------------------------------------------- /template/{% if repo_platform == 'gitlab' or repo_platform == 'gitlab-self-managed' %}.gitlab{% endif %}/workflows/devcontainer.yml.jinja: -------------------------------------------------------------------------------- 1 | {% from pathjoin("includes", "version_compare.jinja") import version_between %} 2 | dev-container-publish: 3 | image: docker:29.1.1@sha256:9b20eb23e1f0443655673efb9db76c4b18cc1b45de1fcf82b3c1b749b9647bdf 4 | parallel: 5 | matrix: 6 | - PYTHON_VERSION: 7 | {% if version_between("3.9", min_py, max_py) %} 8 | - '3.9' 9 | {% endif %} 10 | {% if version_between("3.10", min_py, max_py) %} 11 | - '3.10' 12 | {% endif %} 13 | {% if version_between("3.11", min_py, max_py) %} 14 | - '3.11' 15 | {% endif %} 16 | {% if version_between("3.12", min_py, max_py) %} 17 | - '3.12' 18 | {% endif %} 19 | {% if version_between("3.13", min_py, max_py) %} 20 | - '3.13' 21 | {% endif %} 22 | rules: 23 | - changes: 24 | - .devcontainer/Dockerfile 25 | - .devcontainer/Dockerfile.dockerignore 26 | - .gitlab/workflows/devcontainer.yml 27 | - Makefile 28 | if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH && $CI_PIPELINE_SOURCE == "push" 29 | - if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH && $CI_PIPELINE_SOURCE == "web" 30 | - changes: 31 | - .devcontainer/Dockerfile 32 | - .devcontainer/Dockerfile.dockerignore 33 | - .gitlab/workflows/devcontainer.yml 34 | - Makefile 35 | if: $CI_PIPELINE_SOURCE == 'merge_request_event' 36 | script: 37 | - docker login -u ${CI_REGISTRY_USER} -p ${CI_REGISTRY_PASSWORD} ${CI_REGISTRY} 38 | - docker context create builder 39 | - docker buildx create builder --name container --driver docker-container --use 40 | - docker buildx inspect --bootstrap --builder container 41 | - | 42 | docker buildx build . \ 43 | --build-arg PYTHON_VERSION=${PYTHON_VERSION} \ 44 | --cache-from type=registry,ref=${CI_REGISTRY_IMAGE}/dev-cache:py${PYTHON_VERSION} \ 45 | --file .devcontainer/Dockerfile \ 46 | --load \ 47 | --tag ${CI_REGISTRY_IMAGE}/dev:py${PYTHON_VERSION} \ 48 | --target dev 49 | - | 50 | docker run --rm \ 51 | -e CI=true \ 52 | -v ${PWD}:/workspace \ 53 | ${CI_REGISTRY_IMAGE}/dev:py${PYTHON_VERSION} \ 54 | make dev lint test doc build 55 | - | 56 | docker buildx build . \ 57 | --build-arg PYTHON_VERSION=${PYTHON_VERSION} \ 58 | --file .devcontainer/Dockerfile \ 59 | --load \ 60 | --tag ${CI_REGISTRY_IMAGE}:py${PYTHON_VERSION} \ 61 | --target prod 62 | - docker run --rm ${CI_REGISTRY_IMAGE}:py${PYTHON_VERSION} 63 | - | 64 | if [ "$CI_PIPELINE_SOURCE" != "merge_request_event" ]; then 65 | docker buildx build . \ 66 | --build-arg PYTHON_VERSION=${PYTHON_VERSION} \ 67 | --cache-to type=registry,ref=${CI_REGISTRY_IMAGE}/dev-cache:py${PYTHON_VERSION},mode=max \ 68 | --file .devcontainer/Dockerfile \ 69 | --push \ 70 | --tag ${CI_REGISTRY_IMAGE}/dev:py${PYTHON_VERSION} \ 71 | --target dev 72 | fi 73 | services: 74 | - docker:29.1.1@sha256:9b20eb23e1f0443655673efb9db76c4b18cc1b45de1fcf82b3c1b749b9647bdf 75 | stage: build 76 | variables: 77 | DOCKER_TLS_CERTDIR: /certs 78 | PYTHON_VERSION: ${PYTHON_VERSION} 79 | SOURCE_DATE_EPOCH: 0 80 | -------------------------------------------------------------------------------- /docs/development/git-workflow.md: -------------------------------------------------------------------------------- 1 | # Git Workflow 2 | 3 | This pages shows the recommended Git workflow to keep the local repository clean and organized while ensuring smooth collaboration among team members. 4 | 5 | ## Prerequisites 6 | 7 | Make sure you have [Git](https://git-scm.com/) (version 2.23 and above) installed and properly configured especially for authentication. 8 | 9 | ## Fork and clone the repository 10 | 11 | Fork the repository to your own namespace, and let us take `https://github.com//ss-python` as example. 12 | 13 | Clone the repository and navigate to the root directory: 14 | 15 | ```shell 16 | git clone git@github.com:/ss-python.git 17 | cd ss-python 18 | ``` 19 | 20 | ## Configure the remote 21 | 22 | Add and update the `upstream` remote repository: 23 | 24 | ```shell 25 | git remote add upstream https://github.com/serious-scaffold/ss-python 26 | git fetch upstream 27 | ``` 28 | 29 | Configure `git` to pull `main` branch from the `upstream` remote: 30 | 31 | ```shell 32 | git config --local branch.main.remote upstream 33 | ``` 34 | 35 | Configure `git` never to push to the `upstream` remote: 36 | 37 | ```shell 38 | git remote set-url --push upstream git@github.com//ss-python.git 39 | ``` 40 | 41 | ## Verify the remote configuration 42 | 43 | List the remote repositories with urls: 44 | 45 | ```shell 46 | git remote -v 47 | ``` 48 | 49 | You should have two remote repositories: `origin` to your forked CPython repository, and `upstream` pointing to the official CPython repository: 50 | 51 | ```shell 52 | origin git@github.com:/ss-python.git (fetch) 53 | origin git@github.com:/ss-python.git (push) 54 | upstream https://github.com/serious-scaffold/ss-python (fetch) 55 | upstream git@github.com:/ss-python.git (push) 56 | ``` 57 | 58 | Note that the push url of `upstream` repository is the forked repository. 59 | 60 | Show the upstream for `main` branch: 61 | 62 | ```shell 63 | git config branch.main.remote 64 | ``` 65 | 66 | You should see `upstream` here. 67 | 68 | ## Work on a feature branch 69 | 70 | Create and switch to a new branch from `main`: 71 | 72 | ```shell 73 | git switch -c main 74 | ``` 75 | 76 | Stage the changed files: 77 | 78 | ```shell 79 | git add -p # to review and add changes to existing files 80 | git add # to add new files 81 | ``` 82 | 83 | Commit the staged files: 84 | 85 | ```shell 86 | git commit -m "the commit message" 87 | ``` 88 | 89 | Push the committed changes: 90 | 91 | ```shell 92 | git push 93 | ``` 94 | 95 | ## Create a pull request 96 | 97 | Navigate to the hosting platform and create a pull request. 98 | 99 | After the pull request is merged, you need to delete the branch in your namespace. 100 | 101 | ```{note} 102 | It is recommended to configure the automatic deletion of the merged branches. 103 | ``` 104 | 105 | ## Housekeeping the cloned repository 106 | 107 | Update the `main` branch from upstream: 108 | 109 | ```shell 110 | git switch main 111 | git pull upstream main 112 | ``` 113 | 114 | Remove deleted remote-tracking references: 115 | 116 | ```shell 117 | git fetch --prune origin 118 | ``` 119 | 120 | Remove local branches: 121 | 122 | ```shell 123 | git branch -D 124 | ``` 125 | 126 | After all these operations, you should be ready to again. 127 | 128 | ## Reference 129 | 130 | - [Git bootcamp and cheat sheet, Python Developer's Guide](https://devguide.python.org/getting-started/git-boot-camp/) 131 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | build-backend = "setuptools.build_meta" 3 | requires = [ 4 | "setuptools==80.9.0", 5 | "setuptools-scm==9.2.2", 6 | ] 7 | 8 | [project] 9 | name = "ss-python" 10 | description = "A Python Project Template for Long-Term Maintainability." 11 | readme = "README.md" 12 | keywords = [ 13 | "copier-template", 14 | "long-term-maintainability", 15 | "project-template", 16 | "serious-scaffold", 17 | ] 18 | license = { text = "MIT" } 19 | authors = [ 20 | { email = "i@huxuan.org", name = "huxuan" }, 21 | ] 22 | requires-python = ">=3.9" 23 | classifiers = [ 24 | "Development Status :: 3 - Alpha", 25 | "License :: OSI Approved :: MIT License", 26 | "Operating System :: MacOS :: MacOS X", 27 | "Operating System :: Microsoft :: Windows", 28 | "Operating System :: POSIX :: Linux", 29 | "Programming Language :: Python :: 3 :: Only", 30 | "Programming Language :: Python :: 3.9", 31 | "Programming Language :: Python :: 3.10", 32 | "Programming Language :: Python :: 3.11", 33 | "Programming Language :: Python :: 3.12", 34 | "Programming Language :: Python :: 3.13", 35 | ] 36 | dynamic = [ 37 | "version", 38 | ] 39 | dependencies = [ 40 | "click>=8.1.8", 41 | "pydantic-settings>=2.7.1", 42 | ] 43 | urls.documentation = "https://serious-scaffold.github.io/ss-python" 44 | urls.issue = "https://github.com/serious-scaffold/ss-python/issues" 45 | urls.repository = "https://github.com/serious-scaffold/ss-python" 46 | scripts.ss-python-cli = "ss_python.cli:cli" 47 | 48 | [dependency-groups] 49 | test = [ 50 | "coverage>=7.6.10", 51 | "pytest>=8.3.4", 52 | ] 53 | doc = [ 54 | "autodoc-pydantic>=2.2.0", 55 | "coverage>=7.6.10", 56 | "furo>=2024.8.6", 57 | "mypy[reports]>=1.14.1", 58 | "myst-parser>=3.0.1", 59 | "pytest>=8.3.4", 60 | "sphinx>=7.4.7", 61 | "sphinx-click>=6.0.0", 62 | "sphinx-design>=0.6.1", 63 | ] 64 | lint = [ 65 | "mypy>=1.14.1", 66 | ] 67 | 68 | [tool.pdm] 69 | distribution = true 70 | 71 | [tool.setuptools_scm] 72 | fallback_version = "0.0.0" 73 | 74 | [tool.ruff] 75 | src = [ 76 | "src", 77 | ] 78 | extend-exclude = [ 79 | "template", 80 | ] 81 | fix = true 82 | lint.select = [ 83 | "B", # flake8-bugbear 84 | "D", # pydocstyle 85 | "E", # pycodestyle error 86 | "F", # Pyflakes 87 | "I", # isort 88 | "RUF100", # Unused noqa directive 89 | "S", # flake8-bandit 90 | "SIM", # flake8-simplify 91 | "UP", # pyupgrade 92 | "W", # pycodestyle warning 93 | ] 94 | lint.per-file-ignores."tests/*" = [ 95 | "S101", 96 | ] 97 | lint.pydocstyle.convention = "google" 98 | 99 | [tool.codespell] 100 | write-changes = true 101 | check-filenames = true 102 | 103 | [tool.pyproject-fmt] 104 | indent = 4 105 | keep_full_version = true 106 | max_supported_python = "3.13" 107 | 108 | [tool.pytest.ini_options] 109 | addopts = "-l -s --durations=0" 110 | log_cli = true 111 | log_cli_level = "info" 112 | log_date_format = "%Y-%m-%d %H:%M:%S" 113 | log_format = "%(asctime)s %(levelname)s %(message)s" 114 | minversion = "6.0" 115 | 116 | [tool.coverage.report] 117 | fail_under = 100 118 | 119 | [tool.coverage.run] 120 | source = [ 121 | "ss_python", 122 | ] 123 | 124 | [tool.mypy] 125 | check_untyped_defs = true 126 | disallow_any_unimported = true 127 | disallow_untyped_defs = true 128 | enable_error_code = [ 129 | "ignore-without-code", 130 | ] 131 | exclude = [ 132 | "build", 133 | "doc", 134 | "template", 135 | ] 136 | no_implicit_optional = true 137 | show_error_codes = true 138 | warn_return_any = true 139 | warn_unused_ignores = true 140 | -------------------------------------------------------------------------------- /template/docs/development/git-workflow.md.jinja: -------------------------------------------------------------------------------- 1 | {% from pathjoin("includes", "variable.jinja") import repo_url with context %} 2 | # Git Workflow 3 | 4 | This pages shows the recommended Git workflow to keep the local repository clean and organized while ensuring smooth collaboration among team members. 5 | 6 | ## Prerequisites 7 | 8 | Make sure you have [Git](https://git-scm.com/) (version 2.23 and above) installed and properly configured especially for authentication. 9 | 10 | ## Fork and clone the repository 11 | 12 | Fork the repository to your own namespace, and let us take `https://{{ repo_host }}//{{ repo_name }}` as example. 13 | 14 | Clone the repository and navigate to the root directory: 15 | 16 | ```shell 17 | git clone git@{{ repo_host }}:/{{ repo_name }}.git 18 | cd {{ repo_name }} 19 | ``` 20 | 21 | ## Configure the remote 22 | 23 | Add and update the `upstream` remote repository: 24 | 25 | ```shell 26 | git remote add upstream https://{{ repo_url() }} 27 | git fetch upstream 28 | ``` 29 | 30 | Configure `git` to pull `main` branch from the `upstream` remote: 31 | 32 | ```shell 33 | git config --local branch.main.remote upstream 34 | ``` 35 | 36 | Configure `git` never to push to the `upstream` remote: 37 | 38 | ```shell 39 | git remote set-url --push upstream git@{{ repo_host }}//{{ repo_name }}.git 40 | ``` 41 | 42 | ## Verify the remote configuration 43 | 44 | List the remote repositories with urls: 45 | 46 | ```shell 47 | git remote -v 48 | ``` 49 | 50 | You should have two remote repositories: `origin` to your forked CPython repository, and `upstream` pointing to the official CPython repository: 51 | 52 | ```shell 53 | origin git@{{ repo_host }}:/{{ repo_name }}.git (fetch) 54 | origin git@{{ repo_host }}:/{{ repo_name }}.git (push) 55 | upstream https://{{ repo_url() }} (fetch) 56 | upstream git@{{ repo_host }}:/{{ repo_name }}.git (push) 57 | ``` 58 | 59 | Note that the push url of `upstream` repository is the forked repository. 60 | 61 | Show the upstream for `main` branch: 62 | 63 | ```shell 64 | git config branch.main.remote 65 | ``` 66 | 67 | You should see `upstream` here. 68 | 69 | ## Work on a feature branch 70 | 71 | Create and switch to a new branch from `main`: 72 | 73 | ```shell 74 | git switch -c main 75 | ``` 76 | 77 | Stage the changed files: 78 | 79 | ```shell 80 | git add -p # to review and add changes to existing files 81 | git add # to add new files 82 | ``` 83 | 84 | Commit the staged files: 85 | 86 | ```shell 87 | git commit -m "the commit message" 88 | ``` 89 | 90 | Push the committed changes: 91 | 92 | ```shell 93 | git push 94 | ``` 95 | 96 | ## Create a pull request 97 | 98 | Navigate to the hosting platform and create a pull request. 99 | 100 | After the pull request is merged, you need to delete the branch in your namespace. 101 | 102 | ```{note} 103 | It is recommended to configure the automatic deletion of the merged branches. 104 | ``` 105 | 106 | ## Housekeeping the cloned repository 107 | 108 | Update the `main` branch from upstream: 109 | 110 | ```shell 111 | git switch main 112 | git pull upstream main 113 | ``` 114 | 115 | Remove deleted remote-tracking references: 116 | 117 | ```shell 118 | git fetch --prune origin 119 | ``` 120 | 121 | Remove local branches: 122 | 123 | ```shell 124 | git branch -D 125 | ``` 126 | 127 | After all these operations, you should be ready to again. 128 | 129 | ## Reference 130 | 131 | - [Git bootcamp and cheat sheet, Python Developer's Guide](https://devguide.python.org/getting-started/git-boot-camp/) 132 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | default_install_hook_types: 2 | - post-checkout 3 | - post-merge 4 | - post-rewrite 5 | - pre-push 6 | default_stages: 7 | - manual 8 | - pre-push 9 | repos: 10 | - repo: https://github.com/pre-commit/pre-commit-hooks 11 | rev: v6.0.0 12 | hooks: 13 | - id: check-added-large-files 14 | - id: check-docstring-first 15 | - id: check-merge-conflict 16 | args: 17 | - '--assume-in-merge' 18 | - id: check-toml 19 | - id: check-xml 20 | - id: check-yaml 21 | - id: end-of-file-fixer 22 | - id: forbid-new-submodules 23 | - id: mixed-line-ending 24 | - id: name-tests-test 25 | - id: no-commit-to-branch 26 | stages: 27 | - pre-push 28 | - id: sort-simple-yaml 29 | files: .pre-commit-config.yaml 30 | - id: trailing-whitespace 31 | - repo: https://github.com/renovatebot/pre-commit-hooks 32 | rev: 42.30.2 33 | hooks: 34 | - id: renovate-config-validator 35 | - repo: local 36 | hooks: 37 | - id: pdm-sync 38 | name: pdm-sync 39 | entry: pdm sync 40 | language: python 41 | stages: 42 | - post-checkout 43 | - post-merge 44 | - post-rewrite 45 | always_run: true 46 | pass_filenames: false 47 | - id: pdm-dev-sync 48 | name: pdm-dev-sync 49 | entry: pdm sync --lockfile pdm.dev.lock 50 | language: python 51 | stages: 52 | - post-checkout 53 | - post-merge 54 | - post-rewrite 55 | always_run: true 56 | pass_filenames: false 57 | - id: pdm-lock-check 58 | name: pdm-lock-check 59 | entry: pdm lock --check 60 | language: python 61 | files: ^pyproject.toml$ 62 | pass_filenames: false 63 | - id: pdm-dev-lock-check 64 | name: pdm-dev-lock-check 65 | entry: pdm lock --check --lockfile pdm.dev.lock 66 | language: python 67 | files: ^pyproject.toml$ 68 | pass_filenames: false 69 | - id: mypy 70 | name: mypy 71 | entry: pdm run python -m mypy 72 | language: system 73 | exclude: ^template/.* 74 | types_or: 75 | - python 76 | - pyi 77 | require_serial: true 78 | - id: ruff 79 | name: ruff 80 | entry: ruff check --force-exclude 81 | language: system 82 | types_or: 83 | - python 84 | - pyi 85 | require_serial: true 86 | - id: ruff-format 87 | name: ruff-format 88 | entry: ruff format --force-exclude 89 | language: system 90 | types_or: 91 | - python 92 | - pyi 93 | require_serial: true 94 | - id: pyproject-fmt 95 | name: pyproject-fmt 96 | entry: pyproject-fmt 97 | language: python 98 | files: '(^|/)pyproject\.toml$' 99 | types: 100 | - toml 101 | - id: codespell 102 | name: codespell 103 | entry: codespell 104 | language: python 105 | types: 106 | - text 107 | - id: check-jsonschema 108 | name: check-jsonschema 109 | entry: make check-jsonschema 110 | language: python 111 | files: (?x)^( 112 | \.github/workflows/[^/]+| 113 | \.gitlab-ci\.yml| 114 | \.gitlab/workflows/[^/]+| 115 | \.readthedocs\.yaml| 116 | \.renovaterc\.json 117 | )$ 118 | pass_filenames: false 119 | - id: forbidden-files 120 | name: forbidden files 121 | entry: found Copier update rejection files; review them and remove them 122 | language: fail 123 | files: \.rej$ 124 | -------------------------------------------------------------------------------- /template/{% if repo_platform == 'github' %}.github{% endif %}/workflows/devcontainer.yml.jinja: -------------------------------------------------------------------------------- 1 | {% from pathjoin("includes", "version_compare.jinja") import version_between %} 2 | name: DevContainer 3 | 4 | on: 5 | pull_request: 6 | paths: 7 | - .devcontainer/Dockerfile 8 | - .devcontainer/Dockerfile.dockerignore 9 | - .github/workflows/devcontainer.yml 10 | - Makefile 11 | push: 12 | branches: 13 | - main 14 | paths: 15 | - .devcontainer/Dockerfile 16 | - .devcontainer/Dockerfile.dockerignore 17 | - .github/workflows/devcontainer.yml 18 | - Makefile 19 | workflow_dispatch: null 20 | 21 | concurrency: 22 | cancel-in-progress: true 23 | group: {{ '${{ github.workflow }}-${{ github.ref }}' }} 24 | 25 | jobs: 26 | dev-container-publish: 27 | permissions: 28 | packages: write 29 | runs-on: ubuntu-24.04 30 | steps: 31 | - run: env | sort 32 | - name: Checkout repository 33 | uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 34 | - name: Set up authentication 35 | run: docker login -u {{ '${{ github.actor }}' }} -p {{ '${{ secrets.GITHUB_TOKEN }}' }} ghcr.io 36 | - name: Set up BuildKit 37 | run: | 38 | docker context create builder 39 | docker buildx create builder --name container --driver docker-container --use 40 | docker buildx inspect --bootstrap --builder container 41 | - name: Build the dev container 42 | run: | 43 | docker buildx build . \ 44 | --build-arg PYTHON_VERSION={{ '${{ matrix.python-version }}' }} \ 45 | --cache-from type=registry,ref=ghcr.io/{{ '${{ github.repository }}' }}/dev-cache:py{{ '${{ matrix.python-version }}' }} \ 46 | --file .devcontainer/Dockerfile \ 47 | --load \ 48 | --tag ghcr.io/{{ '${{ github.repository }}' }}/dev:py{{ '${{ matrix.python-version }}' }} \ 49 | --target dev 50 | - name: Test the dev container 51 | run: | 52 | docker run --rm \ 53 | -e CI=true \ 54 | -v ${PWD}:/workspace \ 55 | ghcr.io/{{ '${{ github.repository }}' }}/dev:py{{ '${{ matrix.python-version }}' }} \ 56 | make dev lint test doc build 57 | - name: Build the prod container 58 | run: | 59 | docker buildx build . \ 60 | --build-arg PYTHON_VERSION={{ '${{ matrix.python-version }}' }} \ 61 | --file .devcontainer/Dockerfile \ 62 | --load \ 63 | --tag ghcr.io/{{ '${{ github.repository }}' }}:py{{ '${{ matrix.python-version }}' }} \ 64 | --target prod 65 | - name: Test the prod container 66 | run: docker run --rm ghcr.io/{{ '${{ github.repository }}' }}:py{{ '${{ matrix.python-version }}' }} 67 | - name: Push the dev container 68 | if: github.event_name != 'pull_request' 69 | run: | 70 | docker buildx build . \ 71 | --build-arg PYTHON_VERSION={{ '${{ matrix.python-version }}' }} \ 72 | --cache-to type=registry,ref=ghcr.io/{{ '${{ github.repository }}' }}/dev-cache:py{{ '${{ matrix.python-version }}' }},mode=max \ 73 | --file .devcontainer/Dockerfile \ 74 | --push \ 75 | --tag ghcr.io/{{ '${{ github.repository }}' }}/dev:py{{ '${{ matrix.python-version }}' }} \ 76 | --target dev 77 | strategy: 78 | matrix: 79 | python-version: 80 | {% if version_between("3.9", min_py, max_py) %} 81 | - '3.9' 82 | {% endif %} 83 | {% if version_between("3.10", min_py, max_py) %} 84 | - '3.10' 85 | {% endif %} 86 | {% if version_between("3.11", min_py, max_py) %} 87 | - '3.11' 88 | {% endif %} 89 | {% if version_between("3.12", min_py, max_py) %} 90 | - '3.12' 91 | {% endif %} 92 | {% if version_between("3.13", min_py, max_py) %} 93 | - '3.13' 94 | {% endif %} 95 | -------------------------------------------------------------------------------- /docs/management/update.md: -------------------------------------------------------------------------------- 1 | # Template and Dependency Update 2 | 3 | ## Template update 4 | 5 | To update the project template, thanks to the [update feature](https://copier.readthedocs.io/en/stable/updating/) provided by [Copier](https://github.com/copier-org/copier) and the [regex manager](https://docs.renovatebot.com/modules/manager/regex/) provided by Renovate, a pull request will be automatically created when a new version of the template is released. In most cases, Copier will update the project seamlessly. If conflicts arise, they can be resolved manually since everything is version-controlled by Git. 6 | 7 | ### Tips to minimize potential conflicts 8 | 9 | To minimize potential conflicts, consider the following suggestions: 10 | 11 | 1. Avoid modifying the auto-generated files unless necessary. 12 | 1. For template-related changes, consider proposing an issue or a pull request to the [project template repository](http://github.com/serious-scaffold/ss-python) directly. 13 | 1. For project-specific changes, adopt an inheritance or extension approach to minimize modifications to auto-generated content. 14 | 15 | ## Dependency update 16 | 17 | With the integration of [Renovate](https://github.com/renovatebot/renovate), all dependencies, including those used for development and CI/CD, will be automatically updated via pull requests whenever a new version is released. This allows us to focus solely on testing to ensure the new versions do not break anything. Moreover, an issue titled "Dependency Dashboard" will be created, so that you can have an overview of the state of all dependencies. 18 | 19 | ### Managed dependency types 20 | 21 | The project template tracks the following dependencies: 22 | 23 | 1. Supported managers other than `regex`: 24 | 1. [pep621](https://docs.renovatebot.com/modules/manager/pep621/): The lock file generated by PDM for both dependencies and development dependencies in `pyproject.toml`. 25 | 1. [github-actions](https://docs.renovatebot.com/modules/manager/github-actions/): Actions, runners and containers in GitHub Actions. 26 | 1. [gitlabci](https://docs.renovatebot.com/modules/manager/gitlabci/): Containers in GitLab CI/CD. 27 | 1. [pre-commit](https://docs.renovatebot.com/modules/manager/pre-commit/): Pre-commit hooks. 28 | 1. Regex manager: 29 | 1. Python packages installed with pip/pipx, listed in the README, DevContainer Dockerfile, GitHub Actions, GitLab CI/CD, ReadTheDocs configuration, Renovate configuration and documentation. 30 | 1. Debian packages installed in the DevContainer Dockerfile. 31 | 1. PDM version specified in the `pdm-project/setup-pdm` GitHub action. 32 | 1. PDM version specified in the renovate constraints. 33 | 1. NPM packages used with npx. 34 | 1. The project template itself. 35 | 36 | ### Add new dependencies 37 | 38 | When adding new dependencies that belong to the managed dependency type mentioned above, it is recommended to pin or lock their versions to ensure they are smoothly managed by Renovate. 39 | 40 | When adding new types of dependencies, it is also recommended to manage them with Renovate. 41 | 42 | - If this follows a common pattern, consider creating an issue or even sending a pull request to project template directly. 43 | - If it is project-specific, you can extend the renovate configuration: 44 | - For supported managers other than `regex`, add them in the Renovate configuration using environment variable `RENOVATE_ENABLED_MANAGERS` in GitHub Actions or GitLab CI/CD and configure them in the `renovaterc.json` under the root directory if needed. 45 | - For `regex` managers, add new entries in the `customManagers` and configure `packageRules` if needed in the `.renovaterc.json`. 46 | 47 | ```{note} 48 | This also adheres to the . 49 | ``` 50 | 51 | ```{note} 52 | For the complete list of supported managers and their corresponding configurations, please refer to the [Managers - Renovate Docs](https://docs.renovatebot.com/modules/manager/). 53 | ``` 54 | -------------------------------------------------------------------------------- /template/docs/management/update.md: -------------------------------------------------------------------------------- 1 | # Template and Dependency Update 2 | 3 | ## Template update 4 | 5 | To update the project template, thanks to the [update feature](https://copier.readthedocs.io/en/stable/updating/) provided by [Copier](https://github.com/copier-org/copier) and the [regex manager](https://docs.renovatebot.com/modules/manager/regex/) provided by Renovate, a pull request will be automatically created when a new version of the template is released. In most cases, Copier will update the project seamlessly. If conflicts arise, they can be resolved manually since everything is version-controlled by Git. 6 | 7 | ### Tips to minimize potential conflicts 8 | 9 | To minimize potential conflicts, consider the following suggestions: 10 | 11 | 1. Avoid modifying the auto-generated files unless necessary. 12 | 1. For template-related changes, consider proposing an issue or a pull request to the [project template repository](http://github.com/serious-scaffold/ss-python) directly. 13 | 1. For project-specific changes, adopt an inheritance or extension approach to minimize modifications to auto-generated content. 14 | 15 | ## Dependency update 16 | 17 | With the integration of [Renovate](https://github.com/renovatebot/renovate), all dependencies, including those used for development and CI/CD, will be automatically updated via pull requests whenever a new version is released. This allows us to focus solely on testing to ensure the new versions do not break anything. Moreover, an issue titled "Dependency Dashboard" will be created, so that you can have an overview of the state of all dependencies. 18 | 19 | ### Managed dependency types 20 | 21 | The project template tracks the following dependencies: 22 | 23 | 1. Supported managers other than `regex`: 24 | 1. [pep621](https://docs.renovatebot.com/modules/manager/pep621/): The lock file generated by PDM for both dependencies and development dependencies in `pyproject.toml`. 25 | 1. [github-actions](https://docs.renovatebot.com/modules/manager/github-actions/): Actions, runners and containers in GitHub Actions. 26 | 1. [gitlabci](https://docs.renovatebot.com/modules/manager/gitlabci/): Containers in GitLab CI/CD. 27 | 1. [pre-commit](https://docs.renovatebot.com/modules/manager/pre-commit/): Pre-commit hooks. 28 | 1. Regex manager: 29 | 1. Python packages installed with pip/pipx, listed in the README, DevContainer Dockerfile, GitHub Actions, GitLab CI/CD, ReadTheDocs configuration, Renovate configuration and documentation. 30 | 1. Debian packages installed in the DevContainer Dockerfile. 31 | 1. PDM version specified in the `pdm-project/setup-pdm` GitHub action. 32 | 1. PDM version specified in the renovate constraints. 33 | 1. NPM packages used with npx. 34 | 1. The project template itself. 35 | 36 | ### Add new dependencies 37 | 38 | When adding new dependencies that belong to the managed dependency type mentioned above, it is recommended to pin or lock their versions to ensure they are smoothly managed by Renovate. 39 | 40 | When adding new types of dependencies, it is also recommended to manage them with Renovate. 41 | 42 | - If this follows a common pattern, consider creating an issue or even sending a pull request to project template directly. 43 | - If it is project-specific, you can extend the renovate configuration: 44 | - For supported managers other than `regex`, add them in the Renovate configuration using environment variable `RENOVATE_ENABLED_MANAGERS` in GitHub Actions or GitLab CI/CD and configure them in the `renovaterc.json` under the root directory if needed. 45 | - For `regex` managers, add new entries in the `customManagers` and configure `packageRules` if needed in the `.renovaterc.json`. 46 | 47 | ```{note} 48 | This also adheres to the . 49 | ``` 50 | 51 | ```{note} 52 | For the complete list of supported managers and their corresponding configurations, please refer to the [Managers - Renovate Docs](https://docs.renovatebot.com/modules/manager/). 53 | ``` 54 | -------------------------------------------------------------------------------- /template/.pre-commit-config.yaml.jinja: -------------------------------------------------------------------------------- 1 | {% from pathjoin("includes", "version_compare.jinja") import version_between %} 2 | default_install_hook_types: 3 | - post-checkout 4 | - post-merge 5 | - post-rewrite 6 | - pre-push 7 | default_stages: 8 | - manual 9 | - pre-push 10 | repos: 11 | - repo: https://github.com/pre-commit/pre-commit-hooks 12 | rev: v6.0.0 13 | hooks: 14 | - id: check-added-large-files 15 | - id: check-docstring-first 16 | - id: check-merge-conflict 17 | args: 18 | - '--assume-in-merge' 19 | - id: check-toml 20 | - id: check-xml 21 | - id: check-yaml 22 | - id: end-of-file-fixer 23 | - id: forbid-new-submodules 24 | - id: mixed-line-ending 25 | - id: name-tests-test 26 | - id: no-commit-to-branch 27 | stages: 28 | - pre-push 29 | - id: sort-simple-yaml 30 | files: .pre-commit-config.yaml 31 | - id: trailing-whitespace 32 | - repo: https://github.com/renovatebot/pre-commit-hooks 33 | rev: 42.30.2 34 | hooks: 35 | - id: renovate-config-validator 36 | - repo: local 37 | hooks: 38 | - id: pdm-sync 39 | name: pdm-sync 40 | entry: pdm sync 41 | language: python 42 | stages: 43 | - post-checkout 44 | - post-merge 45 | - post-rewrite 46 | always_run: true 47 | pass_filenames: false 48 | - id: pdm-dev-sync 49 | name: pdm-dev-sync 50 | entry: pdm sync --lockfile pdm.dev.lock 51 | language: python 52 | stages: 53 | - post-checkout 54 | - post-merge 55 | - post-rewrite 56 | always_run: true 57 | pass_filenames: false 58 | - id: pdm-lock-check 59 | name: pdm-lock-check 60 | entry: pdm lock --check 61 | language: python 62 | files: ^pyproject.toml$ 63 | pass_filenames: false 64 | - id: pdm-dev-lock-check 65 | name: pdm-dev-lock-check 66 | entry: pdm lock --check --lockfile pdm.dev.lock 67 | language: python 68 | files: ^pyproject.toml$ 69 | pass_filenames: false 70 | - id: mypy 71 | name: mypy 72 | entry: pdm run python -m mypy 73 | language: system 74 | {% if project_name == 'Serious Scaffold Python' %} 75 | exclude: ^template/.* 76 | {% endif %} 77 | types_or: 78 | - python 79 | - pyi 80 | require_serial: true 81 | - id: ruff 82 | name: ruff 83 | entry: ruff check --force-exclude 84 | language: system 85 | types_or: 86 | - python 87 | - pyi 88 | require_serial: true 89 | - id: ruff-format 90 | name: ruff-format 91 | entry: ruff format --force-exclude 92 | language: system 93 | types_or: 94 | - python 95 | - pyi 96 | require_serial: true 97 | - id: pyproject-fmt 98 | name: pyproject-fmt 99 | entry: pyproject-fmt 100 | language: python 101 | files: '(^|/)pyproject\.toml$' 102 | types: 103 | - toml 104 | - id: codespell 105 | name: codespell 106 | entry: codespell 107 | language: python 108 | types: 109 | - text 110 | - id: check-jsonschema 111 | name: check-jsonschema 112 | entry: make check-jsonschema 113 | language: python 114 | files: (?x)^( 115 | \.github/workflows/[^/]+| 116 | \.gitlab-ci\.yml| 117 | \.gitlab/workflows/[^/]+| 118 | \.readthedocs\.yaml| 119 | \.renovaterc\.json 120 | )$ 121 | pass_filenames: false 122 | - id: forbidden-files 123 | name: forbidden files 124 | entry: found Copier update rejection files; review them and remove them 125 | language: fail 126 | files: \.rej$ 127 | -------------------------------------------------------------------------------- /includes/variable.jinja: -------------------------------------------------------------------------------- 1 | {% macro coverage_badge() %} 2 | {% if repo_platform == 'github' %} 3 | [![Coverage](https://img.shields.io/endpoint?url=https://{{ page_url() }}/_static/badges/coverage.json)](https://{{ page_url() }}/reports/coverage) 4 | {%- elif repo_platform == 'gitlab' or repo_platform == 'gitlab-self-managed' %} 5 | [![coverage report](https://{{ repo_url() }}/badges/main/coverage.svg)](https://{{ repo_url() }}/-/commits/main) 6 | {%- endif %} 7 | {% endmacro %} 8 | 9 | {% macro license_badge() %} 10 | {% if repo_platform == 'github' %} 11 | [![GitHub](https://img.shields.io/github/license/{{ repo_namespace }}/{{ repo_name }})]({{ license_url() }}) 12 | {%- elif repo_platform == 'gitlab' or repo_platform == 'gitlab-self-managed' %} 13 | [![GitLab](https://img.shields.io/gitlab/license/{{ repo_namespace }}/{{ repo_name }}?gitlab_url=https%3A%2F%2F{{ repo_host }})]({{ license_url() }}) 14 | {%- endif %} 15 | {% endmacro %} 16 | 17 | {% macro license_url() %} 18 | {% if repo_platform == 'github' %} 19 | https://{{ repo_url() }}/blob/main/LICENSE 20 | {%- elif repo_platform == 'gitlab' or repo_platform == 'gitlab-self-managed' %} 21 | https://{{ repo_url() }}/-/blob/main/LICENSE 22 | {%- endif %} 23 | {% endmacro %} 24 | 25 | {% macro logo_badge() %} 26 | [![Serious Scaffold Python]({{ logo_badge_url() }})](https://serious-scaffold.github.io/ss-python) 27 | {%- endmacro %} 28 | 29 | {% macro logo_badge_url() %} 30 | https://img.shields.io/endpoint?url=https://serious-scaffold.github.io/ss-python/_static/badges/logo.json 31 | {%- endmacro %} 32 | 33 | {% macro page_url() %} 34 | {% if repo_platform == 'github' %} 35 | {{ repo_namespace }}.github.io/{{ repo_name }} 36 | {%- elif repo_platform == 'gitlab' or repo_platform == 'gitlab-self-managed' %} 37 | {% set repo_namespace_root = repo_namespace.split("/")[0] %} 38 | {{ repo_namespace_root }}.{{ page_host }}{{ repo_namespace | replace(repo_namespace_root, "", 1) }}/{{ repo_name }} 39 | {%- endif %} 40 | {% endmacro %} 41 | 42 | {% macro pipeline_badge() %} 43 | {% if repo_platform == 'github' %} 44 | [![CI](https://{{ repo_url() }}/actions/workflows/ci.yml/badge.svg)](https://{{ repo_url() }}/actions/workflows/ci.yml) 45 | [![CommitLint](https://{{ repo_url() }}/actions/workflows/commitlint.yml/badge.svg)](https://{{ repo_url() }}/actions/workflows/commitlint.yml) 46 | [![DevContainer](https://{{ repo_url() }}/actions/workflows/devcontainer.yml/badge.svg)](https://{{ repo_url() }}/actions/workflows/devcontainer.yml) 47 | [![Release](https://{{ repo_url() }}/actions/workflows/release.yml/badge.svg)](https://{{ repo_url() }}/actions/workflows/release.yml) 48 | [![Renovate](https://{{ repo_url() }}/actions/workflows/renovate.yml/badge.svg)](https://{{ repo_url() }}/actions/workflows/renovate.yml) 49 | [![Semantic Release](https://{{ repo_url() }}/actions/workflows/semantic-release.yml/badge.svg)](https://{{ repo_url() }}/actions/workflows/semantic-release.yml) 50 | {%- elif repo_platform == 'gitlab' or repo_platform == 'gitlab-self-managed' %} 51 | [![pipeline status](https://{{ repo_url() }}/badges/main/pipeline.svg)](https://{{ repo_url() }}/-/commits/main) 52 | {%- endif %} 53 | {% endmacro %} 54 | 55 | {% macro releases_url() %} 56 | {% if repo_platform == 'github' %} 57 | https://{{ repo_url() }}/releases 58 | {%- elif repo_platform == 'gitlab' or repo_platform == 'gitlab-self-managed' %} 59 | https://{{ repo_url() }}/-/releases 60 | {%- endif %} 61 | {% endmacro %} 62 | 63 | {% macro release_badge() %} 64 | {% if repo_platform == 'github' %} 65 | [![Release](https://img.shields.io/github/v/release/{{ repo_namespace }}/{{ repo_name }})] 66 | {%- elif repo_platform == 'gitlab' or repo_platform == 'gitlab-self-managed' %} 67 | [![Latest Release](https://{{ repo_url() }}/-/badges/release.svg)] 68 | {%- endif %} 69 | ({{ releases_url()}}) 70 | [![PyPI](https://img.shields.io/pypi/v/{{ package_name }})](https://pypi.org/project/{{ package_name }}/) 71 | {%- endmacro %} 72 | 73 | {% macro repo_url() %} 74 | {{ repo_host }}/{{ repo_namespace }}/{{ repo_name }} 75 | {%- endmacro %} 76 | -------------------------------------------------------------------------------- /.devcontainer/Dockerfile: -------------------------------------------------------------------------------- 1 | # syntax=docker/dockerfile:1 2 | 3 | ARG PYTHON_VERSION=3.12 4 | 5 | ######################################################################################## 6 | # Dev image is used for development and cicd. 7 | ######################################################################################## 8 | 9 | FROM python:${PYTHON_VERSION} AS dev 10 | 11 | # NOTE: python docker image has env `PYTHON_VERSION` but with patch version. 12 | # ARG is used here for temporary override without changing the original env. 13 | ARG PYTHON_VERSION 14 | 15 | # Config Python 16 | ENV PYTHONDONTWRITEBYTECODE=1 17 | ENV PYTHONHASHSEED=0 18 | ENV PYTHONUNBUFFERED=1 19 | 20 | # Config pipx 21 | ENV PIPX_HOME=/usr/local/pipx 22 | ENV PIPX_BIN_DIR=/usr/local/bin 23 | ENV PIPX_DEFAULT_PYTHON=/usr/local/bin/python 24 | 25 | # renovate: depName=debian_12/bash-completion 26 | ARG BASH_COMPLETION_VERSION="1:2.11-6" 27 | # renovate: depName=debian_12/pipx 28 | ARG PIPX_VERSION="1.1.0-1" 29 | # renovate: depName=debian_12/sudo 30 | ARG SUDO_VERSION="1.9.13p3-1+deb12u2" 31 | # renovate: depName=debian_12/vim 32 | ARG VIM_VERSION="2:9.0.1378-2+deb12u2" 33 | 34 | # Install system dependencies and override pipx with a newer version 35 | RUN apt-get update && apt-get install -y --no-install-recommends \ 36 | bash-completion="${BASH_COMPLETION_VERSION}" \ 37 | pipx="${PIPX_VERSION}" \ 38 | sudo="${SUDO_VERSION}" \ 39 | vim="${VIM_VERSION}" \ 40 | && pipx install pipx==1.8.0 \ 41 | && apt-get purge -y --autoremove pipx \ 42 | && apt-get clean -y \ 43 | && rm -rf /var/lib/apt/lists/* \ 44 | && hash -r 45 | 46 | # Install prerequisites 47 | RUN --mount=source=Makefile,target=Makefile \ 48 | make prerequisites 49 | 50 | # Create a non-root user with sudo permission 51 | ARG USERNAME=ss-python 52 | ARG USER_UID=1000 53 | ARG USER_GID=$USER_UID 54 | 55 | RUN groupadd --gid $USER_GID $USERNAME \ 56 | && useradd --create-home --uid $USER_UID --gid $USER_GID $USERNAME -s /bin/bash \ 57 | && echo $USERNAME ALL=\(root\) NOPASSWD:ALL > /etc/sudoers.d/$USERNAME \ 58 | && chmod 0440 /etc/sudoers.d/$USERNAME 59 | 60 | # Set permission for related folders 61 | RUN chown -R $USER_UID:$USER_GID $PIPX_HOME $PIPX_BIN_DIR 62 | 63 | # Set default working directory 64 | WORKDIR /workspace 65 | 66 | ######################################################################################## 67 | # Build image is an intermediate image used for building the project. 68 | ######################################################################################## 69 | 70 | FROM dev AS build 71 | 72 | # Install dependencies and project into the local packages directory. 73 | ARG SCM_VERSION 74 | RUN --mount=source=README.md,target=README.md \ 75 | --mount=source=pdm.lock,target=pdm.lock \ 76 | --mount=source=pyproject.toml,target=pyproject.toml \ 77 | --mount=source=src,target=src,rw \ 78 | mkdir __pypackages__ && SETUPTOOLS_SCM_PRETEND_VERSION_FOR_SS_PYTHON=${SCM_VERSION} pdm sync --prod --no-editable 79 | 80 | ######################################################################################## 81 | # Prod image is used for deployment and distribution. 82 | ######################################################################################## 83 | 84 | FROM python:${PYTHON_VERSION}-slim AS prod 85 | 86 | # NOTE: python docker image has env `PYTHON_VERSION` but with patch version. 87 | # ARG is used here for temporary override without changing the original env. 88 | ARG PYTHON_VERSION 89 | 90 | # Config Python 91 | ENV PYTHONDONTWRITEBYTECODE=1 92 | ENV PYTHONHASHSEED=0 93 | ENV PYTHONUNBUFFERED=1 94 | 95 | # Retrieve packages from build stage. 96 | ENV PYTHONPATH=/workspace/pkgs 97 | COPY --from=build /workspace/__pypackages__/${PYTHON_VERSION}/lib /workspace/pkgs 98 | 99 | # Retrieve executables from build stage. 100 | COPY --from=build /workspace/__pypackages__/${PYTHON_VERSION}/bin/* /usr/local/bin/ 101 | 102 | # Set command to run the cli by default. 103 | ENTRYPOINT ["ss-python-cli"] 104 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Custom 2 | *.swp 3 | .DS_Store 4 | .copier-answers.yml 5 | Pipfile 6 | public 7 | 8 | # Byte-compiled / optimized / DLL files 9 | __pycache__/ 10 | *.py[cod] 11 | *$py.class 12 | 13 | # C extensions 14 | *.so 15 | 16 | # Distribution / packaging 17 | .Python 18 | build/ 19 | develop-eggs/ 20 | dist/ 21 | downloads/ 22 | eggs/ 23 | .eggs/ 24 | lib/ 25 | lib64/ 26 | parts/ 27 | sdist/ 28 | var/ 29 | wheels/ 30 | share/python-wheels/ 31 | *.egg-info/ 32 | .installed.cfg 33 | *.egg 34 | MANIFEST 35 | 36 | # PyInstaller 37 | # Usually these files are written by a python script from a template 38 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 39 | *.manifest 40 | *.spec 41 | 42 | # Installer logs 43 | pip-log.txt 44 | pip-delete-this-directory.txt 45 | 46 | # Unit test / coverage reports 47 | htmlcov/ 48 | .tox/ 49 | .nox/ 50 | .coverage 51 | .coverage.* 52 | .cache 53 | nosetests.xml 54 | coverage.xml 55 | *.cover 56 | *.py,cover 57 | .hypothesis/ 58 | .pytest_cache/ 59 | cover/ 60 | 61 | # Translations 62 | *.mo 63 | *.pot 64 | 65 | # Django stuff: 66 | *.log 67 | local_settings.py 68 | db.sqlite3 69 | db.sqlite3-journal 70 | 71 | # Flask stuff: 72 | instance/ 73 | .webassets-cache 74 | 75 | # Scrapy stuff: 76 | .scrapy 77 | 78 | # Sphinx documentation 79 | docs/_build/ 80 | 81 | # PyBuilder 82 | .pybuilder/ 83 | target/ 84 | 85 | # Jupyter Notebook 86 | .ipynb_checkpoints 87 | 88 | # IPython 89 | profile_default/ 90 | ipython_config.py 91 | 92 | # pyenv 93 | # For a library or package, you might want to ignore these files since the code is 94 | # intended to run in multiple environments; otherwise, check them in: 95 | # .python-version 96 | 97 | # pipenv 98 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 99 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 100 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 101 | # install all needed dependencies. 102 | #Pipfile.lock 103 | 104 | # poetry 105 | # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. 106 | # This is especially recommended for binary packages to ensure reproducibility, and is more 107 | # commonly ignored for libraries. 108 | # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control 109 | #poetry.lock 110 | 111 | # pdm 112 | # Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. 113 | #pdm.lock 114 | # pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it 115 | # in version control. 116 | # https://pdm.fming.dev/latest/usage/project/#working-with-version-control 117 | .pdm.toml 118 | .pdm-python 119 | .pdm-build/ 120 | 121 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm 122 | __pypackages__/ 123 | 124 | # Celery stuff 125 | celerybeat-schedule 126 | celerybeat.pid 127 | 128 | # SageMath parsed files 129 | *.sage.py 130 | 131 | # Environments 132 | .env 133 | .venv 134 | env/ 135 | venv/ 136 | ENV/ 137 | env.bak/ 138 | venv.bak/ 139 | 140 | # Spyder project settings 141 | .spyderproject 142 | .spyproject 143 | 144 | # Rope project settings 145 | .ropeproject 146 | 147 | # mkdocs documentation 148 | /site 149 | 150 | # mypy 151 | .mypy_cache/ 152 | .dmypy.json 153 | dmypy.json 154 | 155 | # Pyre type checker 156 | .pyre/ 157 | 158 | # pytype static type analyzer 159 | .pytype/ 160 | 161 | # Cython debug symbols 162 | cython_debug/ 163 | 164 | # PyCharm 165 | # JetBrains specific template is maintained in a separate JetBrains.gitignore that can 166 | # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore 167 | # and can be added to the global gitignore or merged into this file. For a more nuclear 168 | # option (not recommended) you can uncomment the following to ignore the entire idea folder. 169 | #.idea/ 170 | -------------------------------------------------------------------------------- /docs/management/release.md: -------------------------------------------------------------------------------- 1 | # Release Process 2 | 3 | With the integration of [semantic-release](https://github.com/semantic-release/semantic-release), the release process is fully automated. To enable this, follow the settings for . Besides, adhering to the is strongly recommended to ensure the release process works as expected. 4 | 5 | ## Release Configuration 6 | 7 | The release configuration is located in the root directory of the project: 8 | 9 | ```{literalinclude} ../../.releaserc.json 10 | ``` 11 | 12 | Based on this configuration, the following trigger rules apply: 13 | 14 | * A **major** release is triggered by a 'BREAKING CHANGE' or 'BREAKING-CHANGE' in the footer or has a `major-release` scope. 15 | * A **minor** release is triggered when the commit type is `feat` or has a `minor-release` scope. 16 | * A **patch** release is triggered when the commit type is `fix`, `perf`, `refactor` or `revert` or has a `patch-release` scope. 17 | * No release is triggered if the commit type is any other type or has a `no-release` scope. 18 | 19 | ## Commit message examples 20 | 21 | ### Major release 22 | 23 | * ```text 24 | feat: drop Python 3.8 support 25 | 26 | BREAKING CHANGE: drop Python 3.8 support 27 | ``` 28 | * `chore(major-release): a major release` 29 | 30 | ### Minor release 31 | 32 | * `feat: add an awesome feature` 33 | * `chore(minor-release): a minor release` 34 | 35 | ### Patch release 36 | 37 | * `fix: fix a silly bug` 38 | * `perf: performance improvement for the core` 39 | * `refactor: refactor the base module` 40 | * `revert: revert a buggy implementation` 41 | * `chore(patch-release): a patch release` 42 | 43 | ### No release 44 | 45 | * `feat(no-release): a feature that should not trigger a release` 46 | * `fix(no-release,core): a fix that should not trigger a release, but with more scopes` 47 | 48 | ## Release Tasks 49 | 50 | The release process includes the following tasks: 51 | 52 | ::::{tab-set} 53 | 54 | :::{tab-item} GitHub 55 | :sync: github 56 | 57 | 1. Generate a changelog from unreleased commits. 58 | 1. Publish a new GitHub Release and semantic version tag. 59 | 1. Build and publish the documentation to GitHub Pages. 60 | 1. Build and publish the Python package to the configured package repository. 61 | 1. Build and publish the Development and Production Containers with the build cache to GitHub Packages. 62 | 1. The Production Container is tagged as `ghcr.io/serious-scaffold/ss-python:py` for the latest version and `ghcr.io/serious-scaffold/ss-python:py-` for archives. 63 | 1. The Development Container is tagged as `ghcr.io/serious-scaffold/ss-python/dev:py` for the latest version and `ghcr.io/serious-scaffold/ss-python/dev:py-` for archives. 64 | 1. The build cache for the Development Container is tagged as `ghcr.io/serious-scaffold/ss-python/dev-cache:py`. 65 | 66 | ::: 67 | 68 | :::{tab-item} GitLab 69 | :sync: gitlab 70 | 71 | 1. Generate a changelog from unreleased commits. 72 | 1. Publish a new GitLab Release and semantic version tag. 73 | 1. Build and publish the documentation to GitLab Pages. 74 | 1. Build and publish the Python package to the configured package repository. 75 | 1. Build and publish the Development and Production Containers with build cache to GitLab Container Registry. 76 | 1. The Production Container is tagged as `registry.gitlab.com/serious-scaffold/ss-python:py` for the latest version and `registry.gitlab.com/serious-scaffold/ss-python:py-` for archives. 77 | 1. The Development Container is tagged as `registry.gitlab.com/serious-scaffold/ss-python/dev:py` for the latest version and `registry.gitlab.com/serious-scaffold/ss-python/dev:py-` for archives. 78 | 1. The build cache for the Development Container is tagged as `registry.gitlab.com/serious-scaffold/ss-python/dev-cache:py`. 79 | 80 | ::: 81 | 82 | :::: 83 | -------------------------------------------------------------------------------- /template/.gitignore.jinja: -------------------------------------------------------------------------------- 1 | # Custom 2 | *.swp 3 | .DS_Store 4 | {% if project_name == "Serious Scaffold Python" %} 5 | .copier-answers.yml 6 | {% endif %} 7 | Pipfile 8 | public 9 | 10 | # Byte-compiled / optimized / DLL files 11 | __pycache__/ 12 | *.py[cod] 13 | *$py.class 14 | 15 | # C extensions 16 | *.so 17 | 18 | # Distribution / packaging 19 | .Python 20 | build/ 21 | develop-eggs/ 22 | dist/ 23 | downloads/ 24 | eggs/ 25 | .eggs/ 26 | lib/ 27 | lib64/ 28 | parts/ 29 | sdist/ 30 | var/ 31 | wheels/ 32 | share/python-wheels/ 33 | *.egg-info/ 34 | .installed.cfg 35 | *.egg 36 | MANIFEST 37 | 38 | # PyInstaller 39 | # Usually these files are written by a python script from a template 40 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 41 | *.manifest 42 | *.spec 43 | 44 | # Installer logs 45 | pip-log.txt 46 | pip-delete-this-directory.txt 47 | 48 | # Unit test / coverage reports 49 | htmlcov/ 50 | .tox/ 51 | .nox/ 52 | .coverage 53 | .coverage.* 54 | .cache 55 | nosetests.xml 56 | coverage.xml 57 | *.cover 58 | *.py,cover 59 | .hypothesis/ 60 | .pytest_cache/ 61 | cover/ 62 | 63 | # Translations 64 | *.mo 65 | *.pot 66 | 67 | # Django stuff: 68 | *.log 69 | local_settings.py 70 | db.sqlite3 71 | db.sqlite3-journal 72 | 73 | # Flask stuff: 74 | instance/ 75 | .webassets-cache 76 | 77 | # Scrapy stuff: 78 | .scrapy 79 | 80 | # Sphinx documentation 81 | docs/_build/ 82 | 83 | # PyBuilder 84 | .pybuilder/ 85 | target/ 86 | 87 | # Jupyter Notebook 88 | .ipynb_checkpoints 89 | 90 | # IPython 91 | profile_default/ 92 | ipython_config.py 93 | 94 | # pyenv 95 | # For a library or package, you might want to ignore these files since the code is 96 | # intended to run in multiple environments; otherwise, check them in: 97 | # .python-version 98 | 99 | # pipenv 100 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 101 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 102 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 103 | # install all needed dependencies. 104 | #Pipfile.lock 105 | 106 | # poetry 107 | # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. 108 | # This is especially recommended for binary packages to ensure reproducibility, and is more 109 | # commonly ignored for libraries. 110 | # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control 111 | #poetry.lock 112 | 113 | # pdm 114 | # Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. 115 | #pdm.lock 116 | # pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it 117 | # in version control. 118 | # https://pdm.fming.dev/latest/usage/project/#working-with-version-control 119 | .pdm.toml 120 | .pdm-python 121 | .pdm-build/ 122 | 123 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm 124 | __pypackages__/ 125 | 126 | # Celery stuff 127 | celerybeat-schedule 128 | celerybeat.pid 129 | 130 | # SageMath parsed files 131 | *.sage.py 132 | 133 | # Environments 134 | .env 135 | .venv 136 | env/ 137 | venv/ 138 | ENV/ 139 | env.bak/ 140 | venv.bak/ 141 | 142 | # Spyder project settings 143 | .spyderproject 144 | .spyproject 145 | 146 | # Rope project settings 147 | .ropeproject 148 | 149 | # mkdocs documentation 150 | /site 151 | 152 | # mypy 153 | .mypy_cache/ 154 | .dmypy.json 155 | dmypy.json 156 | 157 | # Pyre type checker 158 | .pyre/ 159 | 160 | # pytype static type analyzer 161 | .pytype/ 162 | 163 | # Cython debug symbols 164 | cython_debug/ 165 | 166 | # PyCharm 167 | # JetBrains specific template is maintained in a separate JetBrains.gitignore that can 168 | # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore 169 | # and can be added to the global gitignore or merged into this file. For a more nuclear 170 | # option (not recommended) you can uncomment the following to ignore the entire idea folder. 171 | #.idea/ 172 | -------------------------------------------------------------------------------- /template/.devcontainer/Dockerfile.jinja: -------------------------------------------------------------------------------- 1 | # syntax=docker/dockerfile:1 2 | 3 | ARG PYTHON_VERSION={{ default_py }} 4 | 5 | ######################################################################################## 6 | # Dev image is used for development and cicd. 7 | ######################################################################################## 8 | 9 | FROM python:${PYTHON_VERSION} AS dev 10 | 11 | # NOTE: python docker image has env `PYTHON_VERSION` but with patch version. 12 | # ARG is used here for temporary override without changing the original env. 13 | ARG PYTHON_VERSION 14 | 15 | # Config Python 16 | ENV PYTHONDONTWRITEBYTECODE=1 17 | ENV PYTHONHASHSEED=0 18 | ENV PYTHONUNBUFFERED=1 19 | 20 | # Config pipx 21 | ENV PIPX_HOME=/usr/local/pipx 22 | ENV PIPX_BIN_DIR=/usr/local/bin 23 | ENV PIPX_DEFAULT_PYTHON=/usr/local/bin/python 24 | 25 | # renovate: depName=debian_12/bash-completion 26 | ARG BASH_COMPLETION_VERSION="1:2.11-6" 27 | # renovate: depName=debian_12/pipx 28 | ARG PIPX_VERSION="1.1.0-1" 29 | # renovate: depName=debian_12/sudo 30 | ARG SUDO_VERSION="1.9.13p3-1+deb12u2" 31 | # renovate: depName=debian_12/vim 32 | ARG VIM_VERSION="2:9.0.1378-2+deb12u2" 33 | 34 | # Install system dependencies and override pipx with a newer version 35 | RUN apt-get update && apt-get install -y --no-install-recommends \ 36 | bash-completion="${BASH_COMPLETION_VERSION}" \ 37 | pipx="${PIPX_VERSION}" \ 38 | sudo="${SUDO_VERSION}" \ 39 | vim="${VIM_VERSION}" \ 40 | && pipx install pipx==1.8.0 \ 41 | && apt-get purge -y --autoremove pipx \ 42 | && apt-get clean -y \ 43 | && rm -rf /var/lib/apt/lists/* \ 44 | && hash -r 45 | 46 | # Install prerequisites 47 | RUN --mount=source=Makefile,target=Makefile \ 48 | make prerequisites 49 | 50 | # Create a non-root user with sudo permission 51 | ARG USERNAME={{ repo_name }} 52 | ARG USER_UID=1000 53 | ARG USER_GID=$USER_UID 54 | 55 | RUN groupadd --gid $USER_GID $USERNAME \ 56 | && useradd --create-home --uid $USER_UID --gid $USER_GID $USERNAME -s /bin/bash \ 57 | && echo $USERNAME ALL=\(root\) NOPASSWD:ALL > /etc/sudoers.d/$USERNAME \ 58 | && chmod 0440 /etc/sudoers.d/$USERNAME 59 | 60 | # Set permission for related folders 61 | RUN chown -R $USER_UID:$USER_GID $PIPX_HOME $PIPX_BIN_DIR 62 | 63 | # Set default working directory 64 | WORKDIR /workspace 65 | 66 | ######################################################################################## 67 | # Build image is an intermediate image used for building the project. 68 | ######################################################################################## 69 | 70 | FROM dev AS build 71 | 72 | # Install dependencies and project into the local packages directory. 73 | ARG SCM_VERSION 74 | RUN --mount=source=README.md,target=README.md \ 75 | --mount=source=pdm.lock,target=pdm.lock \ 76 | --mount=source=pyproject.toml,target=pyproject.toml \ 77 | --mount=source=src,target=src,rw \ 78 | mkdir __pypackages__ && SETUPTOOLS_SCM_PRETEND_VERSION_FOR_{{ package_name|upper|replace(".", "_")|replace("-", "_") }}=${SCM_VERSION} pdm sync --prod --no-editable 79 | 80 | ######################################################################################## 81 | # Prod image is used for deployment and distribution. 82 | ######################################################################################## 83 | 84 | FROM python:${PYTHON_VERSION}-slim AS prod 85 | 86 | # NOTE: python docker image has env `PYTHON_VERSION` but with patch version. 87 | # ARG is used here for temporary override without changing the original env. 88 | ARG PYTHON_VERSION 89 | 90 | # Config Python 91 | ENV PYTHONDONTWRITEBYTECODE=1 92 | ENV PYTHONHASHSEED=0 93 | ENV PYTHONUNBUFFERED=1 94 | 95 | # Retrieve packages from build stage. 96 | ENV PYTHONPATH=/workspace/pkgs 97 | COPY --from=build /workspace/__pypackages__/${PYTHON_VERSION}/lib /workspace/pkgs 98 | 99 | # Retrieve executables from build stage. 100 | COPY --from=build /workspace/__pypackages__/${PYTHON_VERSION}/bin/* /usr/local/bin/ 101 | 102 | # Set command to run the cli by default. 103 | ENTRYPOINT ["{{ package_name }}-cli"] 104 | -------------------------------------------------------------------------------- /template/docs/management/release.md.jinja: -------------------------------------------------------------------------------- 1 | # Release Process 2 | 3 | With the integration of [semantic-release](https://github.com/semantic-release/semantic-release), the release process is fully automated. To enable this, follow the settings for . Besides, adhering to the is strongly recommended to ensure the release process works as expected. 4 | 5 | ## Release Configuration 6 | 7 | The release configuration is located in the root directory of the project: 8 | 9 | ```{literalinclude} ../../.releaserc.json 10 | ``` 11 | 12 | Based on this configuration, the following trigger rules apply: 13 | 14 | * A **major** release is triggered by a 'BREAKING CHANGE' or 'BREAKING-CHANGE' in the footer or has a `major-release` scope. 15 | * A **minor** release is triggered when the commit type is `feat` or has a `minor-release` scope. 16 | * A **patch** release is triggered when the commit type is `fix`, `perf`, `refactor` or `revert` or has a `patch-release` scope. 17 | * No release is triggered if the commit type is any other type or has a `no-release` scope. 18 | 19 | ## Commit message examples 20 | 21 | ### Major release 22 | 23 | * ```text 24 | feat: drop Python 3.8 support 25 | 26 | BREAKING CHANGE: drop Python 3.8 support 27 | ``` 28 | * `chore(major-release): a major release` 29 | 30 | ### Minor release 31 | 32 | * `feat: add an awesome feature` 33 | * `chore(minor-release): a minor release` 34 | 35 | ### Patch release 36 | 37 | * `fix: fix a silly bug` 38 | * `perf: performance improvement for the core` 39 | * `refactor: refactor the base module` 40 | * `revert: revert a buggy implementation` 41 | * `chore(patch-release): a patch release` 42 | 43 | ### No release 44 | 45 | * `feat(no-release): a feature that should not trigger a release` 46 | * `fix(no-release,core): a fix that should not trigger a release, but with more scopes` 47 | 48 | ## Release Tasks 49 | 50 | The release process includes the following tasks: 51 | 52 | ::::{tab-set} 53 | 54 | :::{tab-item} GitHub 55 | :sync: github 56 | 57 | 1. Generate a changelog from unreleased commits. 58 | 1. Publish a new GitHub Release and semantic version tag. 59 | 1. Build and publish the documentation to GitHub Pages. 60 | 1. Build and publish the Python package to the configured package repository. 61 | 1. Build and publish the Development and Production Containers with the build cache to GitHub Packages. 62 | 1. The Production Container is tagged as `ghcr.io/{{ repo_namespace }}/{{ repo_name }}:py` for the latest version and `ghcr.io/{{ repo_namespace }}/{{ repo_name }}:py-` for archives. 63 | 1. The Development Container is tagged as `ghcr.io/{{ repo_namespace }}/{{ repo_name }}/dev:py` for the latest version and `ghcr.io/{{ repo_namespace }}/{{ repo_name }}/dev:py-` for archives. 64 | 1. The build cache for the Development Container is tagged as `ghcr.io/{{ repo_namespace }}/{{ repo_name }}/dev-cache:py`. 65 | 66 | ::: 67 | 68 | :::{tab-item} GitLab 69 | :sync: gitlab 70 | 71 | 1. Generate a changelog from unreleased commits. 72 | 1. Publish a new GitLab Release and semantic version tag. 73 | 1. Build and publish the documentation to GitLab Pages. 74 | 1. Build and publish the Python package to the configured package repository. 75 | 1. Build and publish the Development and Production Containers with build cache to GitLab Container Registry. 76 | 1. The Production Container is tagged as `registry.gitlab.com/{{ repo_namespace }}/{{ repo_name }}:py` for the latest version and `registry.gitlab.com/{{ repo_namespace }}/{{ repo_name }}:py-` for archives. 77 | 1. The Development Container is tagged as `registry.gitlab.com/{{ repo_namespace }}/{{ repo_name }}/dev:py` for the latest version and `registry.gitlab.com/{{ repo_namespace }}/{{ repo_name }}/dev:py-` for archives. 78 | 1. The build cache for the Development Container is tagged as `registry.gitlab.com/{{ repo_namespace }}/{{ repo_name }}/dev-cache:py`. 79 | 80 | ::: 81 | 82 | :::: 83 | -------------------------------------------------------------------------------- /.gitlab/workflows/release.yml: -------------------------------------------------------------------------------- 1 | pages-build: 2 | artifacts: 3 | paths: 4 | - public 5 | cache: 6 | paths: 7 | - .venv 8 | key: 9 | files: 10 | - pdm.dev.lock 11 | - pdm.lock 12 | prefix: venv-${PYTHON_VERSION} 13 | policy: pull 14 | rules: 15 | - if: $CI_COMMIT_TAG =~ /^v?(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)(-?(a|b|rc)(0|[1-9][0-9]*)?)?$/ 16 | script: 17 | - make dev-doc 18 | - make doc 19 | stage: release 20 | pages: 21 | artifacts: 22 | paths: 23 | - public 24 | needs: 25 | - pages-build 26 | rules: 27 | - if: $CI_COMMIT_TAG =~ /^v?(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)(-?(a|b|rc)(0|[1-9][0-9]*)?)?$/ 28 | script: 29 | - echo "Running the pages job." 30 | stage: release 31 | variables: 32 | GIT_STRATEGY: none 33 | container-publish: 34 | image: docker:29.1.1@sha256:9b20eb23e1f0443655673efb9db76c4b18cc1b45de1fcf82b3c1b749b9647bdf 35 | parallel: 36 | matrix: 37 | - PYTHON_VERSION: 38 | - '3.9' 39 | - '3.10' 40 | - '3.11' 41 | - '3.12' 42 | - '3.13' 43 | rules: 44 | - if: $CI_COMMIT_TAG =~ /^v?(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)(-?(a|b|rc)(0|[1-9][0-9]*)?)?$/ 45 | script: 46 | - docker login -u ${CI_REGISTRY_USER} -p ${CI_REGISTRY_PASSWORD} ${CI_REGISTRY} 47 | - docker context create builder 48 | - docker buildx create builder --name container --driver docker-container --use 49 | - docker buildx inspect --bootstrap --builder container 50 | - | 51 | docker buildx build . \ 52 | --build-arg PYTHON_VERSION=${PYTHON_VERSION} \ 53 | --cache-from type=registry,ref=${CI_REGISTRY_IMAGE}/dev-cache:py${PYTHON_VERSION} \ 54 | --file .devcontainer/Dockerfile \ 55 | --load \ 56 | --tag ${CI_REGISTRY_IMAGE}/dev:py${PYTHON_VERSION} \ 57 | --tag ${CI_REGISTRY_IMAGE}/dev:py${PYTHON_VERSION}-${CI_COMMIT_TAG} \ 58 | --target dev 59 | - | 60 | docker run --rm \ 61 | -e CI=true \ 62 | -v ${PWD}:/workspace \ 63 | ${CI_REGISTRY_IMAGE}/dev:py${PYTHON_VERSION} \ 64 | make dev lint test doc build 65 | - | 66 | docker buildx build . \ 67 | --build-arg SCM_VERSION=${CI_COMMIT_TAG} \ 68 | --build-arg PYTHON_VERSION=${PYTHON_VERSION} \ 69 | --file .devcontainer/Dockerfile \ 70 | --load \ 71 | --tag ${CI_REGISTRY_IMAGE}:py${PYTHON_VERSION} \ 72 | --tag ${CI_REGISTRY_IMAGE}:py${PYTHON_VERSION}-${CI_COMMIT_TAG} \ 73 | --target prod 74 | - docker run --rm ${CI_REGISTRY_IMAGE}:py${PYTHON_VERSION} 75 | - | 76 | docker buildx build . \ 77 | --build-arg PYTHON_VERSION=${PYTHON_VERSION} \ 78 | --cache-to type=registry,ref=${CI_REGISTRY_IMAGE}/dev-cache:py${PYTHON_VERSION},mode=max \ 79 | --file .devcontainer/Dockerfile \ 80 | --push \ 81 | --tag ${CI_REGISTRY_IMAGE}/dev:py${PYTHON_VERSION} \ 82 | --tag ${CI_REGISTRY_IMAGE}/dev:py${PYTHON_VERSION}-${CI_COMMIT_TAG} \ 83 | --target dev 84 | - | 85 | docker buildx build . \ 86 | --build-arg SCM_VERSION=${CI_COMMIT_TAG} \ 87 | --build-arg PYTHON_VERSION=${PYTHON_VERSION} \ 88 | --file .devcontainer/Dockerfile \ 89 | --push \ 90 | --tag ${CI_REGISTRY_IMAGE}:py${PYTHON_VERSION} \ 91 | --tag ${CI_REGISTRY_IMAGE}:py${PYTHON_VERSION}-${CI_COMMIT_TAG} \ 92 | --target prod 93 | services: 94 | - docker:29.1.1@sha256:9b20eb23e1f0443655673efb9db76c4b18cc1b45de1fcf82b3c1b749b9647bdf 95 | stage: release 96 | variables: 97 | DOCKER_TLS_CERTDIR: /certs 98 | PYTHON_VERSION: ${PYTHON_VERSION} 99 | SOURCE_DATE_EPOCH: 0 100 | package-publish: 101 | cache: 102 | paths: 103 | - .venv 104 | key: 105 | files: 106 | - pdm.dev.lock 107 | - pdm.lock 108 | prefix: venv-${PYTHON_VERSION} 109 | policy: pull 110 | rules: 111 | - if: $CI_COMMIT_TAG =~ /^v?(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)(-?(a|b|rc)(0|[1-9][0-9]*)?)?$/ 112 | script: 113 | - make publish 114 | stage: release 115 | -------------------------------------------------------------------------------- /includes/sample.jinja: -------------------------------------------------------------------------------- 1 | {% macro readme_content() %} 2 | {% from pathjoin("includes", "variable.jinja") import page_url with context %} 3 | {% from pathjoin("includes", "variable.jinja") import repo_url with context %} 4 | [![{{ project_name }}](https://{{ page_url() }}/_static/images/logo.svg)](https://{{ repo_url() }}) 5 | 6 | Serious Scaffold Python is crafted for long-term, maintainable Python projects. It includes GitHub Actions and GitLab CI/CD, automated dependency updates via Renovate, and comprehensive linting, testing, and documentation. Key integrations like pdm for environment and dependency management, click for CLI development, and pydantic for configuration enhance project robustness. With copier’s easy project setup and continuous updating, your project stays aligned with best practices for sustainable development. Pre-configured dev containers and cross-platform CI support ensure maintainability from the start. 7 | 8 | {% if repo_platform == 'github' %} 9 | If you find this helpful, please consider [sponsorship](https://github.com/sponsors/{{ author_name }}). 10 | {% endif %} 11 | 12 | ## 🛠️ Features 13 | 14 | - Project setup and template update with [copier](https://copier.readthedocs.io/). 15 | - Manage dependencies and virtual environments with [pdm](https://pdm-project.org/). 16 | - Build with [setuptools](https://github.com/pypa/setuptools) and versioned with [setuptools-scm](https://github.com/pypa/setuptools_scm/). 17 | - Lint with [pre-commit](https://pre-commit.com), [mypy](http://www.mypy-lang.org/), [ruff](https://github.com/charliermarsh/ruff), [pyproject-fmt](https://github.com/tox-dev/pyproject-fmt), [codespell](https://github.com/codespell-project/codespell), [check-jsonschema](https://github.com/python-jsonschema/check-jsonschema) and [commitlint](https://commitlint.js.org/). 18 | - Test with [pytest](https://docs.pytest.org/) and [coverage](https://coverage.readthedocs.io) for threshold and reports. 19 | - Documentation with [sphinx](https://www.sphinx-doc.org/), the [furo](https://pradyunsg.me/furo) theme and [MyST parser](https://myst-parser.readthedocs.io/) for markdown. 20 | - Build documentation dynamically when related files change with [watchfiles](https://github.com/samuelcolvin/watchfiles). 21 | - Develop Command Line Interfaces with [click](https://click.palletsprojects.com/). 22 | - Manage configurations with [pydantic-settings](https://docs.pydantic.dev/latest/usage/pydantic_settings/). 23 | - [Dev container](https://containers.dev/) for development and GitLab CI/CD. 24 | - Automate dependency updates with [Renovate](https://github.com/renovatebot/renovate). 25 | - Automate version management and release with [semantic-release](https://github.com/semantic-release/semantic-release). 26 | - [Versioned documentation](https://docs.readthedocs.io/en/stable/versions.html) and [pull request previews](https://docs.readthedocs.io/en/stable/pull-requests.html) with [Read the Docs](https://readthedocs.org/). 27 | - Adapted configuration for GitHub, GitLab and self-managed GitLab. 28 | - Continuous Integration for Linux, MacOS and Windows [GitHub Only]. 29 | - Continuous Integration for multiple Python versions. 30 | - Release with documentation, package and production container. 31 | - Centralize common actions with a unified Makefile. 32 | - VSCode settings with recommended extensions. 33 | 34 | ## 🔧 Prerequisites 35 | 36 | [pipx](https://pipx.pypa.io/) is required to manage the standalone tools used across the development lifecycle. 37 | Please refer to pipx's installation instructions [here](https://pipx.pypa.io/stable/installation/). 38 | Once pipx is set up, install the copier for project generation using the following command: 39 | 40 | ```bash 41 | pipx install copier==9.8.0 42 | ``` 43 | 44 | ## 🚀 Quickstart 45 | 46 | 1. Generate the project. 47 | 48 | ```bash 49 | copier copy gh:serious-scaffold/ss-python /path/to/project 50 | ``` 51 | 52 | 1. Navigate to the project directory and initialize a git repository. 53 | 54 | ```bash 55 | cd /path/to/project 56 | git init 57 | ``` 58 | 59 | 1. Install standalone tools. 60 | 61 | ```bash 62 | make prerequisites 63 | ``` 64 | 65 | 1. Set up the development environment. 66 | 67 | ```bash 68 | make dev 69 | ``` 70 | 71 | 1. Commit the initialized project. 72 | 73 | ```bash 74 | git add . 75 | git commit -m "Initialize from serious-scaffold." 76 | ``` 77 | 78 | 1. That's it! Feel free to focus on the coding within `src` folder. 79 | {% endmacro %} 80 | -------------------------------------------------------------------------------- /template/{% if repo_platform == 'gitlab' or repo_platform == 'gitlab-self-managed' %}.gitlab{% endif %}/workflows/release.yml.jinja: -------------------------------------------------------------------------------- 1 | {% from pathjoin("includes", "version_compare.jinja") import version_between %} 2 | pages-build: 3 | artifacts: 4 | paths: 5 | - public 6 | cache: 7 | paths: 8 | - .venv 9 | key: 10 | files: 11 | - pdm.dev.lock 12 | - pdm.lock 13 | prefix: venv-${PYTHON_VERSION} 14 | policy: pull 15 | rules: 16 | - if: $CI_COMMIT_TAG =~ /^v?(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)(-?(a|b|rc)(0|[1-9][0-9]*)?)?$/ 17 | script: 18 | - make dev-doc 19 | - make doc 20 | stage: release 21 | pages: 22 | artifacts: 23 | paths: 24 | - public 25 | needs: 26 | - pages-build 27 | rules: 28 | - if: $CI_COMMIT_TAG =~ /^v?(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)(-?(a|b|rc)(0|[1-9][0-9]*)?)?$/ 29 | script: 30 | - echo "Running the pages job." 31 | stage: release 32 | variables: 33 | GIT_STRATEGY: none 34 | container-publish: 35 | image: docker:29.1.1@sha256:9b20eb23e1f0443655673efb9db76c4b18cc1b45de1fcf82b3c1b749b9647bdf 36 | parallel: 37 | matrix: 38 | - PYTHON_VERSION: 39 | {% if version_between("3.9", min_py, max_py) %} 40 | - '3.9' 41 | {% endif %} 42 | {% if version_between("3.10", min_py, max_py) %} 43 | - '3.10' 44 | {% endif %} 45 | {% if version_between("3.11", min_py, max_py) %} 46 | - '3.11' 47 | {% endif %} 48 | {% if version_between("3.12", min_py, max_py) %} 49 | - '3.12' 50 | {% endif %} 51 | {% if version_between("3.13", min_py, max_py) %} 52 | - '3.13' 53 | {% endif %} 54 | rules: 55 | - if: $CI_COMMIT_TAG =~ /^v?(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)(-?(a|b|rc)(0|[1-9][0-9]*)?)?$/ 56 | script: 57 | - docker login -u ${CI_REGISTRY_USER} -p ${CI_REGISTRY_PASSWORD} ${CI_REGISTRY} 58 | - docker context create builder 59 | - docker buildx create builder --name container --driver docker-container --use 60 | - docker buildx inspect --bootstrap --builder container 61 | - | 62 | docker buildx build . \ 63 | --build-arg PYTHON_VERSION=${PYTHON_VERSION} \ 64 | --cache-from type=registry,ref=${CI_REGISTRY_IMAGE}/dev-cache:py${PYTHON_VERSION} \ 65 | --file .devcontainer/Dockerfile \ 66 | --load \ 67 | --tag ${CI_REGISTRY_IMAGE}/dev:py${PYTHON_VERSION} \ 68 | --tag ${CI_REGISTRY_IMAGE}/dev:py${PYTHON_VERSION}-${CI_COMMIT_TAG} \ 69 | --target dev 70 | - | 71 | docker run --rm \ 72 | -e CI=true \ 73 | -v ${PWD}:/workspace \ 74 | ${CI_REGISTRY_IMAGE}/dev:py${PYTHON_VERSION} \ 75 | make dev lint test doc build 76 | - | 77 | docker buildx build . \ 78 | --build-arg SCM_VERSION=${CI_COMMIT_TAG} \ 79 | --build-arg PYTHON_VERSION=${PYTHON_VERSION} \ 80 | --file .devcontainer/Dockerfile \ 81 | --load \ 82 | --tag ${CI_REGISTRY_IMAGE}:py${PYTHON_VERSION} \ 83 | --tag ${CI_REGISTRY_IMAGE}:py${PYTHON_VERSION}-${CI_COMMIT_TAG} \ 84 | --target prod 85 | - docker run --rm ${CI_REGISTRY_IMAGE}:py${PYTHON_VERSION} 86 | - | 87 | docker buildx build . \ 88 | --build-arg PYTHON_VERSION=${PYTHON_VERSION} \ 89 | --cache-to type=registry,ref=${CI_REGISTRY_IMAGE}/dev-cache:py${PYTHON_VERSION},mode=max \ 90 | --file .devcontainer/Dockerfile \ 91 | --push \ 92 | --tag ${CI_REGISTRY_IMAGE}/dev:py${PYTHON_VERSION} \ 93 | --tag ${CI_REGISTRY_IMAGE}/dev:py${PYTHON_VERSION}-${CI_COMMIT_TAG} \ 94 | --target dev 95 | - | 96 | docker buildx build . \ 97 | --build-arg SCM_VERSION=${CI_COMMIT_TAG} \ 98 | --build-arg PYTHON_VERSION=${PYTHON_VERSION} \ 99 | --file .devcontainer/Dockerfile \ 100 | --push \ 101 | --tag ${CI_REGISTRY_IMAGE}:py${PYTHON_VERSION} \ 102 | --tag ${CI_REGISTRY_IMAGE}:py${PYTHON_VERSION}-${CI_COMMIT_TAG} \ 103 | --target prod 104 | services: 105 | - docker:29.1.1@sha256:9b20eb23e1f0443655673efb9db76c4b18cc1b45de1fcf82b3c1b749b9647bdf 106 | stage: release 107 | variables: 108 | DOCKER_TLS_CERTDIR: /certs 109 | PYTHON_VERSION: ${PYTHON_VERSION} 110 | SOURCE_DATE_EPOCH: 0 111 | package-publish: 112 | cache: 113 | paths: 114 | - .venv 115 | key: 116 | files: 117 | - pdm.dev.lock 118 | - pdm.lock 119 | prefix: venv-${PYTHON_VERSION} 120 | policy: pull 121 | rules: 122 | - if: $CI_COMMIT_TAG =~ /^v?(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)(-?(a|b|rc)(0|[1-9][0-9]*)?)?$/ 123 | script: 124 | - make publish 125 | stage: release 126 | -------------------------------------------------------------------------------- /.releaserc.json: -------------------------------------------------------------------------------- 1 | { 2 | "plugins": [ 3 | [ 4 | "@semantic-release/commit-analyzer", 5 | { 6 | "releaseRules": [ 7 | { 8 | "breaking": true, 9 | "release": "major" 10 | }, 11 | { 12 | "type": "build", 13 | "release": false 14 | }, 15 | { 16 | "type": "chore", 17 | "release": false 18 | }, 19 | { 20 | "type": "ci", 21 | "release": false 22 | }, 23 | { 24 | "type": "docs", 25 | "release": false 26 | }, 27 | { 28 | "type": "feat", 29 | "release": "minor" 30 | }, 31 | { 32 | "type": "fix", 33 | "release": "patch" 34 | }, 35 | { 36 | "type": "perf", 37 | "release": "patch" 38 | }, 39 | { 40 | "type": "refactor", 41 | "release": false 42 | }, 43 | { 44 | "type": "revert", 45 | "release": "patch" 46 | }, 47 | { 48 | "type": "style", 49 | "release": false 50 | }, 51 | { 52 | "type": "test", 53 | "release": false 54 | }, 55 | { 56 | "scope": "*major-release*", 57 | "release": "major" 58 | }, 59 | { 60 | "scope": "*minor-release*", 61 | "release": "minor" 62 | }, 63 | { 64 | "scope": "*patch-release*", 65 | "release": "patch" 66 | }, 67 | { 68 | "scope": "*no-release*", 69 | "release": false 70 | } 71 | ] 72 | } 73 | ], 74 | [ 75 | "@semantic-release/release-notes-generator", 76 | { 77 | "presetConfig": { 78 | "types": [ 79 | { 80 | "type": "build", 81 | "section": "Build" 82 | }, 83 | { 84 | "type": "chore", 85 | "section": "Chores" 86 | }, 87 | { 88 | "type": "ci", 89 | "section": "Continuous Integration" 90 | }, 91 | { 92 | "type": "docs", 93 | "section": "Documentation" 94 | }, 95 | { 96 | "type": "feat", 97 | "section": "Features" 98 | }, 99 | { 100 | "type": "fix", 101 | "section": "Bug Fixes" 102 | }, 103 | { 104 | "type": "perf", 105 | "section": "Performance" 106 | }, 107 | { 108 | "type": "refactor", 109 | "section": "Refactor" 110 | }, 111 | { 112 | "type": "revert", 113 | "section": "Reverts" 114 | }, 115 | { 116 | "type": "style", 117 | "section": "Styles" 118 | }, 119 | { 120 | "type": "test", 121 | "section": "Tests" 122 | } 123 | ] 124 | } 125 | } 126 | ], 127 | "@semantic-release/github" 128 | ], 129 | "preset": "conventionalcommits" 130 | } 131 | -------------------------------------------------------------------------------- /template/.releaserc.json.jinja: -------------------------------------------------------------------------------- 1 | { 2 | "plugins": [ 3 | [ 4 | "@semantic-release/commit-analyzer", 5 | { 6 | "releaseRules": [ 7 | { 8 | "breaking": true, 9 | "release": "major" 10 | }, 11 | { 12 | "type": "build", 13 | "release": false 14 | }, 15 | { 16 | "type": "chore", 17 | "release": false 18 | }, 19 | { 20 | "type": "ci", 21 | "release": false 22 | }, 23 | { 24 | "type": "docs", 25 | "release": false 26 | }, 27 | { 28 | "type": "feat", 29 | "release": "minor" 30 | }, 31 | { 32 | "type": "fix", 33 | "release": "patch" 34 | }, 35 | { 36 | "type": "perf", 37 | "release": "patch" 38 | }, 39 | { 40 | "type": "refactor", 41 | "release": false 42 | }, 43 | { 44 | "type": "revert", 45 | "release": "patch" 46 | }, 47 | { 48 | "type": "style", 49 | "release": false 50 | }, 51 | { 52 | "type": "test", 53 | "release": false 54 | }, 55 | { 56 | "scope": "*major-release*", 57 | "release": "major" 58 | }, 59 | { 60 | "scope": "*minor-release*", 61 | "release": "minor" 62 | }, 63 | { 64 | "scope": "*patch-release*", 65 | "release": "patch" 66 | }, 67 | { 68 | "scope": "*no-release*", 69 | "release": false 70 | } 71 | ] 72 | } 73 | ], 74 | [ 75 | "@semantic-release/release-notes-generator", 76 | { 77 | "presetConfig": { 78 | "types": [ 79 | { 80 | "type": "build", 81 | "section": "Build" 82 | }, 83 | { 84 | "type": "chore", 85 | "section": "Chores" 86 | }, 87 | { 88 | "type": "ci", 89 | "section": "Continuous Integration" 90 | }, 91 | { 92 | "type": "docs", 93 | "section": "Documentation" 94 | }, 95 | { 96 | "type": "feat", 97 | "section": "Features" 98 | }, 99 | { 100 | "type": "fix", 101 | "section": "Bug Fixes" 102 | }, 103 | { 104 | "type": "perf", 105 | "section": "Performance" 106 | }, 107 | { 108 | "type": "refactor", 109 | "section": "Refactor" 110 | }, 111 | { 112 | "type": "revert", 113 | "section": "Reverts" 114 | }, 115 | { 116 | "type": "style", 117 | "section": "Styles" 118 | }, 119 | { 120 | "type": "test", 121 | "section": "Tests" 122 | } 123 | ] 124 | } 125 | } 126 | ], 127 | {% if repo_platform == 'github' %} 128 | "@semantic-release/github" 129 | {% elif repo_platform == 'gitlab' %} 130 | "@semantic-release/gitlab" 131 | {% elif repo_platform == 'gitlab-self-managed' %} 132 | [ 133 | "@semantic-release/gitlab", 134 | { 135 | "gitlabUrl": "https://{{ repo_host }}" 136 | } 137 | ] 138 | {% endif %} 139 | ], 140 | "preset": "conventionalcommits" 141 | } 142 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | 3 | on: 4 | release: 5 | types: 6 | - published 7 | 8 | permissions: 9 | contents: read 10 | 11 | concurrency: 12 | cancel-in-progress: true 13 | group: ${{ github.workflow }}-${{ github.ref }} 14 | 15 | jobs: 16 | pages-build: 17 | runs-on: ubuntu-24.04 18 | steps: 19 | - name: Checkout repository 20 | uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 21 | with: 22 | fetch-depth: 0 23 | - name: Set up PDM 24 | uses: pdm-project/setup-pdm@94a823180e06fcde4ad29308721954a521c96ed0 # v4.4 25 | with: 26 | cache: true 27 | python-version: "3.12" 28 | version: 2.25.4 29 | cache-dependency-path: | 30 | ./pdm.dev.lock 31 | ./pdm.lock 32 | - run: env | sort 33 | - run: make dev-doc 34 | - run: make doc 35 | - name: Upload pages artifact 36 | uses: actions/upload-pages-artifact@7b1f4a764d45c48632c6b24a0339c27f5614fb0b # v4.0.0 37 | with: 38 | path: public 39 | pages: 40 | needs: 41 | - pages-build 42 | permissions: 43 | id-token: write 44 | pages: write 45 | runs-on: ubuntu-24.04 46 | steps: 47 | - id: deployment 48 | name: Deploy to GitHub Pages 49 | uses: actions/deploy-pages@d6db90164ac5ed86f2b6aed7e0febac5b3c0c03e # v4.0.5 50 | container-publish: 51 | permissions: 52 | packages: write 53 | runs-on: ubuntu-24.04 54 | steps: 55 | - run: env | sort 56 | - name: Checkout repository 57 | uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 58 | - name: Set up authentication 59 | run: docker login -u ${{ github.actor }} -p ${{ secrets.GITHUB_TOKEN }} ghcr.io 60 | - name: Set up BuildKit 61 | run: | 62 | docker context create builder 63 | docker buildx create builder --name container --driver docker-container --use 64 | docker buildx inspect --bootstrap --builder container 65 | - name: Build the dev container 66 | run: | 67 | docker buildx build . \ 68 | --build-arg PYTHON_VERSION=${{ matrix.python-version }} \ 69 | --cache-from type=registry,ref=ghcr.io/${{ github.repository }}/dev-cache:py${{ matrix.python-version }} \ 70 | --file .devcontainer/Dockerfile \ 71 | --load \ 72 | --tag ghcr.io/${{ github.repository }}/dev:py${{ matrix.python-version }} \ 73 | --tag ghcr.io/${{ github.repository }}/dev:py${{ matrix.python-version }}-${{ github.ref_name }} \ 74 | --target dev 75 | - name: Test the dev container 76 | run: | 77 | docker run --rm \ 78 | -e CI=true \ 79 | -v ${PWD}:/workspace \ 80 | ghcr.io/${{ github.repository }}/dev:py${{ matrix.python-version }} \ 81 | make dev lint test doc build 82 | - name: Build the prod container 83 | run: | 84 | docker buildx build . \ 85 | --build-arg SCM_VERSION=${{ github.ref_name }} \ 86 | --build-arg PYTHON_VERSION=${{ matrix.python-version }} \ 87 | --file .devcontainer/Dockerfile \ 88 | --load \ 89 | --tag ghcr.io/${{ github.repository }}:py${{ matrix.python-version }} \ 90 | --tag ghcr.io/${{ github.repository }}:py${{ matrix.python-version }}-${{ github.ref_name }} \ 91 | --target prod 92 | - name: Test the prod container 93 | run: docker run --rm ghcr.io/${{ github.repository }}:py${{ matrix.python-version }} 94 | - name: Push the dev container 95 | run: | 96 | docker buildx build . \ 97 | --build-arg PYTHON_VERSION=${{ matrix.python-version }} \ 98 | --cache-to type=registry,ref=ghcr.io/${{ github.repository }}/dev-cache:py${{ matrix.python-version }},mode=max \ 99 | --file .devcontainer/Dockerfile \ 100 | --push \ 101 | --tag ghcr.io/${{ github.repository }}/dev:py${{ matrix.python-version }} \ 102 | --tag ghcr.io/${{ github.repository }}/dev:py${{ matrix.python-version }}-${{ github.ref_name }} \ 103 | --target dev 104 | - name: Push the prod container 105 | run: | 106 | docker buildx build . \ 107 | --build-arg SCM_VERSION=${{ github.ref_name }} \ 108 | --build-arg PYTHON_VERSION=${{ matrix.python-version }} \ 109 | --file .devcontainer/Dockerfile \ 110 | --push \ 111 | --tag ghcr.io/${{ github.repository }}:py${{ matrix.python-version }} \ 112 | --tag ghcr.io/${{ github.repository }}:py${{ matrix.python-version }}-${{ github.ref_name }} \ 113 | --target prod 114 | strategy: 115 | matrix: 116 | python-version: 117 | - "3.9" 118 | - "3.10" 119 | - "3.11" 120 | - "3.12" 121 | - "3.13" 122 | package-publish: 123 | runs-on: ubuntu-24.04 124 | permissions: 125 | contents: read 126 | id-token: write 127 | steps: 128 | - name: Checkout repository 129 | uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 130 | - name: Set up PDM 131 | uses: pdm-project/setup-pdm@94a823180e06fcde4ad29308721954a521c96ed0 # v4.4 132 | with: 133 | cache: true 134 | python-version: "3.12" 135 | version: 2.25.4 136 | cache-dependency-path: | 137 | ./pdm.dev.lock 138 | ./pdm.lock 139 | - run: env | sort 140 | - env: 141 | PDM_PUBLISH_PASSWORD: ${{ secrets.PDM_PUBLISH_PASSWORD }} 142 | PDM_PUBLISH_USERNAME: ${{ vars.PDM_PUBLISH_USERNAME || '__token__' }} 143 | run: make publish 144 | -------------------------------------------------------------------------------- /template/pyproject.toml.jinja: -------------------------------------------------------------------------------- 1 | {% from pathjoin("includes", "variable.jinja") import page_url with context %} 2 | {% from pathjoin("includes", "variable.jinja") import repo_url with context %} 3 | {% from pathjoin("includes", "version_compare.jinja") import version_between %} 4 | [build-system] 5 | build-backend = "setuptools.build_meta" 6 | requires = [ 7 | "setuptools==80.9.0", 8 | "setuptools-scm==9.2.2", 9 | ] 10 | 11 | [project] 12 | name = "{{ package_name }}" 13 | description = "{{ project_description }}" 14 | readme = "README.md" 15 | keywords = [ 16 | {% set keywords = (project_keywords.split(",") | map("trim") | list) + ["serious-scaffold"] %} 17 | {% for keyword in keywords | sort %} 18 | "{{ keyword }}", 19 | {% endfor %} 20 | ] 21 | {% if copyright_license == "Apache Software License" %} 22 | license = { text = "Apache-2.0" } 23 | {% elif copyright_license == "Boost Software License 1.0 (BSL-1.0)" %} 24 | license = { text = "BSL-1.0" } 25 | {% elif copyright_license == "GNU Affero General Public License v3" %} 26 | license = { text = "AGPLv3" } 27 | {% elif copyright_license == "GNU General Public License v3 (GPLv3)" %} 28 | license = { text = "GPLv3" } 29 | {% elif copyright_license == "GNU Lesser General Public License v3 (LGPLv3)" %} 30 | license = { text = "LGPLv3" } 31 | {% elif copyright_license == "MIT License" %} 32 | license = { text = "MIT" } 33 | {% elif copyright_license == "Mozilla Public License 2.0 (MPL 2.0)" %} 34 | license = { text = "MPL-2.0" } 35 | {% elif copyright_license == "The Unlicense (Unlicense)" %} 36 | license = { text = "Unlicense" } 37 | {% endif %} 38 | authors = [ 39 | { email = "{{ author_email }}", name = "{{ author_name }}" }, 40 | ] 41 | requires-python = ">={{ min_py }}" 42 | classifiers = [ 43 | {% if development_status == "Alpha" %} 44 | "Development Status :: 3 - Alpha", 45 | {% elif development_status == "Beta" %} 46 | "Development Status :: 4 - Beta", 47 | {% elif development_status == "Stable" %} 48 | "Development Status :: 5 - Production/Stable", 49 | {% endif %} 50 | "License :: OSI Approved :: {{ copyright_license }}", 51 | {% if "macos" in platforms %} 52 | "Operating System :: MacOS :: MacOS X", 53 | {% endif %} 54 | {% if "windows" in platforms %} 55 | "Operating System :: Microsoft :: Windows", 56 | {% endif %} 57 | {% if "linux" in platforms %} 58 | "Operating System :: POSIX :: Linux", 59 | {% endif %} 60 | "Programming Language :: Python :: 3 :: Only", 61 | {% if version_between("3.9", min_py, max_py) %} 62 | "Programming Language :: Python :: 3.9", 63 | {% endif %} 64 | {% if version_between("3.10", min_py, max_py) %} 65 | "Programming Language :: Python :: 3.10", 66 | {% endif %} 67 | {% if version_between("3.11", min_py, max_py) %} 68 | "Programming Language :: Python :: 3.11", 69 | {% endif %} 70 | {% if version_between("3.12", min_py, max_py) %} 71 | "Programming Language :: Python :: 3.12", 72 | {% endif %} 73 | {% if version_between("3.13", min_py, max_py) %} 74 | "Programming Language :: Python :: 3.13", 75 | {% endif %} 76 | ] 77 | dynamic = [ 78 | "version", 79 | ] 80 | dependencies = [ 81 | "click>=8.1.8", 82 | "pydantic-settings>=2.7.1", 83 | ] 84 | urls.documentation = "https://{{ page_url() }}" 85 | {% if repo_platform == 'github' %} 86 | urls.issue = "https://{{ repo_url() }}/issues" 87 | {% elif repo_platform == 'gitlab' or repo_platform == 'gitlab-self-managed' %} 88 | urls.issue = "https://{{ repo_url() }}/-/issues" 89 | {% endif %} 90 | urls.repository = "https://{{ repo_url() }}" 91 | scripts.{{ package_name }}-cli = "{{ module_name }}.cli:cli" 92 | 93 | [dependency-groups] 94 | test = [ 95 | "coverage>=7.6.10", 96 | "pytest>=8.3.4", 97 | ] 98 | doc = [ 99 | "autodoc-pydantic>=2.2.0", 100 | "coverage>=7.6.10", 101 | "furo>=2024.8.6", 102 | "mypy[reports]>=1.14.1", 103 | "myst-parser>=3.0.1", 104 | "pytest>=8.3.4", 105 | "sphinx>=7.4.7", 106 | "sphinx-click>=6.0.0", 107 | "sphinx-design>=0.6.1", 108 | ] 109 | lint = [ 110 | "mypy>=1.14.1", 111 | ] 112 | 113 | [tool.pdm] 114 | distribution = true 115 | 116 | [tool.setuptools_scm] 117 | fallback_version = "0.0.0" 118 | 119 | [tool.ruff] 120 | src = [ 121 | "src", 122 | ] 123 | {% if project_name == "Serious Scaffold Python" %} 124 | extend-exclude = [ 125 | "template", 126 | ] 127 | {% endif %} 128 | fix = true 129 | lint.select = [ 130 | "B", # flake8-bugbear 131 | "D", # pydocstyle 132 | "E", # pycodestyle error 133 | "F", # Pyflakes 134 | "I", # isort 135 | "RUF100", # Unused noqa directive 136 | "S", # flake8-bandit 137 | "SIM", # flake8-simplify 138 | "UP", # pyupgrade 139 | "W", # pycodestyle warning 140 | ] 141 | lint.per-file-ignores."tests/*" = [ 142 | "S101", 143 | ] 144 | lint.pydocstyle.convention = "google" 145 | 146 | [tool.codespell] 147 | write-changes = true 148 | check-filenames = true 149 | 150 | [tool.pyproject-fmt] 151 | indent = 4 152 | keep_full_version = true 153 | max_supported_python = "{{ max_py }}" 154 | 155 | [tool.pytest.ini_options] 156 | addopts = "-l -s --durations=0" 157 | log_cli = true 158 | log_cli_level = "info" 159 | log_date_format = "%Y-%m-%d %H:%M:%S" 160 | log_format = "%(asctime)s %(levelname)s %(message)s" 161 | minversion = "6.0" 162 | 163 | [tool.coverage.report] 164 | fail_under = {{ coverage_threshold }} 165 | 166 | [tool.coverage.run] 167 | source = [ 168 | "{{ module_name }}", 169 | ] 170 | 171 | [tool.mypy] 172 | check_untyped_defs = true 173 | disallow_any_unimported = true 174 | disallow_untyped_defs = true 175 | enable_error_code = [ 176 | "ignore-without-code", 177 | ] 178 | exclude = [ 179 | "build", 180 | "doc", 181 | {% if project_name == "Serious Scaffold Python" %} 182 | "template", 183 | {% endif %} 184 | ] 185 | no_implicit_optional = true 186 | show_error_codes = true 187 | warn_return_any = true 188 | warn_unused_ignores = true 189 | -------------------------------------------------------------------------------- /template/{% if repo_platform == 'github' %}.github{% endif %}/workflows/release.yml.jinja: -------------------------------------------------------------------------------- 1 | {% from pathjoin("includes", "version_compare.jinja") import version_between %} 2 | name: Release 3 | 4 | on: 5 | release: 6 | types: 7 | - published 8 | 9 | permissions: 10 | contents: read 11 | 12 | concurrency: 13 | cancel-in-progress: true 14 | group: {{ '${{ github.workflow }}-${{ github.ref }}' }} 15 | 16 | jobs: 17 | pages-build: 18 | runs-on: ubuntu-24.04 19 | steps: 20 | - name: Checkout repository 21 | uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 22 | with: 23 | fetch-depth: 0 24 | - name: Set up PDM 25 | uses: pdm-project/setup-pdm@94a823180e06fcde4ad29308721954a521c96ed0 # v4.4 26 | with: 27 | cache: true 28 | python-version: "{{ default_py }}" 29 | version: 2.25.4 30 | cache-dependency-path: | 31 | ./pdm.dev.lock 32 | ./pdm.lock 33 | - run: env | sort 34 | - run: make dev-doc 35 | - run: make doc 36 | - name: Upload pages artifact 37 | uses: actions/upload-pages-artifact@7b1f4a764d45c48632c6b24a0339c27f5614fb0b # v4.0.0 38 | with: 39 | path: public 40 | pages: 41 | needs: 42 | - pages-build 43 | permissions: 44 | id-token: write 45 | pages: write 46 | runs-on: ubuntu-24.04 47 | steps: 48 | - id: deployment 49 | name: Deploy to GitHub Pages 50 | uses: actions/deploy-pages@d6db90164ac5ed86f2b6aed7e0febac5b3c0c03e # v4.0.5 51 | container-publish: 52 | permissions: 53 | packages: write 54 | runs-on: ubuntu-24.04 55 | steps: 56 | - run: env | sort 57 | - name: Checkout repository 58 | uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 59 | - name: Set up authentication 60 | run: docker login -u {{ '${{ github.actor }}' }} -p {{ '${{ secrets.GITHUB_TOKEN }}' }} ghcr.io 61 | - name: Set up BuildKit 62 | run: | 63 | docker context create builder 64 | docker buildx create builder --name container --driver docker-container --use 65 | docker buildx inspect --bootstrap --builder container 66 | - name: Build the dev container 67 | run: | 68 | docker buildx build . \ 69 | --build-arg PYTHON_VERSION={{ '${{ matrix.python-version }}' }} \ 70 | --cache-from type=registry,ref=ghcr.io/{{ '${{ github.repository }}' }}/dev-cache:py{{ '${{ matrix.python-version }}' }} \ 71 | --file .devcontainer/Dockerfile \ 72 | --load \ 73 | --tag ghcr.io/{{ '${{ github.repository }}' }}/dev:py{{ '${{ matrix.python-version }}' }} \ 74 | --tag ghcr.io/{{ '${{ github.repository }}' }}/dev:py{{ '${{ matrix.python-version }}' }}-{{ '${{ github.ref_name }}' }} \ 75 | --target dev 76 | - name: Test the dev container 77 | run: | 78 | docker run --rm \ 79 | -e CI=true \ 80 | -v ${PWD}:/workspace \ 81 | ghcr.io/{{ '${{ github.repository }}' }}/dev:py{{ '${{ matrix.python-version }}' }} \ 82 | make dev lint test doc build 83 | - name: Build the prod container 84 | run: | 85 | docker buildx build . \ 86 | --build-arg SCM_VERSION={{ '${{ github.ref_name }}' }} \ 87 | --build-arg PYTHON_VERSION={{ '${{ matrix.python-version }}' }} \ 88 | --file .devcontainer/Dockerfile \ 89 | --load \ 90 | --tag ghcr.io/{{ '${{ github.repository }}' }}:py{{ '${{ matrix.python-version }}' }} \ 91 | --tag ghcr.io/{{ '${{ github.repository }}' }}:py{{ '${{ matrix.python-version }}' }}-{{ '${{ github.ref_name }}' }} \ 92 | --target prod 93 | - name: Test the prod container 94 | run: docker run --rm ghcr.io/{{ '${{ github.repository }}' }}:py{{ '${{ matrix.python-version }}' }} 95 | - name: Push the dev container 96 | run: | 97 | docker buildx build . \ 98 | --build-arg PYTHON_VERSION={{ '${{ matrix.python-version }}' }} \ 99 | --cache-to type=registry,ref=ghcr.io/{{ '${{ github.repository }}' }}/dev-cache:py{{ '${{ matrix.python-version }}' }},mode=max \ 100 | --file .devcontainer/Dockerfile \ 101 | --push \ 102 | --tag ghcr.io/{{ '${{ github.repository }}' }}/dev:py{{ '${{ matrix.python-version }}' }} \ 103 | --tag ghcr.io/{{ '${{ github.repository }}' }}/dev:py{{ '${{ matrix.python-version }}' }}-{{ '${{ github.ref_name }}' }} \ 104 | --target dev 105 | - name: Push the prod container 106 | run: | 107 | docker buildx build . \ 108 | --build-arg SCM_VERSION={{ '${{ github.ref_name }}' }} \ 109 | --build-arg PYTHON_VERSION={{ '${{ matrix.python-version }}' }} \ 110 | --file .devcontainer/Dockerfile \ 111 | --push \ 112 | --tag ghcr.io/{{ '${{ github.repository }}' }}:py{{ '${{ matrix.python-version }}' }} \ 113 | --tag ghcr.io/{{ '${{ github.repository }}' }}:py{{ '${{ matrix.python-version }}' }}-{{ '${{ github.ref_name }}' }} \ 114 | --target prod 115 | strategy: 116 | matrix: 117 | python-version: 118 | {% if version_between("3.9", min_py, max_py) %} 119 | - "3.9" 120 | {% endif %} 121 | {% if version_between("3.10", min_py, max_py) %} 122 | - "3.10" 123 | {% endif %} 124 | {% if version_between("3.11", min_py, max_py) %} 125 | - "3.11" 126 | {% endif %} 127 | {% if version_between("3.12", min_py, max_py) %} 128 | - "3.12" 129 | {% endif %} 130 | {% if version_between("3.13", min_py, max_py) %} 131 | - "3.13" 132 | {% endif %} 133 | package-publish: 134 | runs-on: ubuntu-24.04 135 | permissions: 136 | contents: read 137 | id-token: write 138 | steps: 139 | - name: Checkout repository 140 | uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 141 | - name: Set up PDM 142 | uses: pdm-project/setup-pdm@94a823180e06fcde4ad29308721954a521c96ed0 # v4.4 143 | with: 144 | cache: true 145 | python-version: "{{ default_py }}" 146 | version: 2.25.4 147 | cache-dependency-path: | 148 | ./pdm.dev.lock 149 | ./pdm.lock 150 | - run: env | sort 151 | - env: 152 | PDM_PUBLISH_PASSWORD: {{ '${{ secrets.PDM_PUBLISH_PASSWORD }}' }} 153 | PDM_PUBLISH_USERNAME: {{ '${{ vars.PDM_PUBLISH_USERNAME || \'__token__\' }}' }} 154 | run: make publish 155 | --------------------------------------------------------------------------------