Repository: pyscaffold/pyscaffold (language: python)

Issue:
Improvements for Cirrus CI
After having worked on both the GitHub Actions and GitLab CI plugins, I think we also need to improve Cirrus CI 😅

It is a bit behind... The following would be "nice to have" improvements:
- [ ] Upload to coveralls for all tasks using different tags (this relies on Coveralls' "parallel" builds, there is an example on how to do that on the GitlabCI PR https://github.com/pyscaffold/pyscaffold/pull/622)
- [ ] Use the "pipeline" approach: build the package first, then use the same package to test across the different OS/Python versions
- [ ] Add support for publishing releases.
- [ ] It would also be nice to simplify that file... Currently it is too big and too complicated to understand (I guess the culprit is Windows)


Discussion / hints:

Fix patch:
diff --git a/.cirrus.yml b/.cirrus.yml
index e2ac30a21..57f2c7579 100644
--- a/.cirrus.yml
+++ b/.cirrus.yml
@@ -1,53 +1,51 @@
 ---
-# Default values to be merged into tasks:
-auto_cancellation: false
+# ---- Default values to be merged into tasks ----
+
 env:
-  PATH: ${HOME}/.local/bin:${PATH}
-  # ^  add user paths
-  PIP_CACHE: ${HOME}/.cache/pip
-  PRE_COMMIT_HOME: ${HOME}/.cache/pre-commit
   LC_ALL: C.UTF-8
   LANG: C.UTF-8
-  COVERAGE: NO
+  PIP_CACHE_DIR: ${CIRRUS_WORKING_DIR}/.cache/pip
+  PRE_COMMIT_HOME: ${CIRRUS_WORKING_DIR}/.cache/pre-commit
+  # Coveralls configuration
   CI_NAME: cirrus-ci
   CI_BRANCH: ${CIRRUS_BRANCH}
   CI_PULL_REQUEST: ${CIRRUS_PR}
   CI_BUILD_NUMBER: ${CIRRUS_BUILD_ID}
   CI_BUILD_URL: https://cirrus-ci.com/build/${CIRRUS_BUILD_ID}
+  COVERALLS_PARALLEL: "true"
+  COVERALLS_FLAG_NAME: ${CIRRUS_TASK_NAME}
 
+# ---- Templates ----
 
-# This template is used in most of the tasks
-.regular_task_template: &REGULAR_TASK_TEMPLATE
-  tox_install_script:
-    - python -m pip install --upgrade pip setuptools tox pre-commit
-  prepare_script: &prepare
-    # This script is also used in Windows, so the shell is not POSIX
+.task_template: &task-template
+  debug_information_script:
+    - echo "$(which python) -- $(python -VV)"
+    - echo "$(which pip) -- $(pip -VV)"
+    - python -c 'import os, sys; print(os.name, sys.platform, getattr(sys, "abiflags", None))'
+  prepare_script:  # avoid git failing with setuptools-scm
     - git config --global user.email "you@example.com"
     - git config --global user.name "Your Name"
-  clean_workspace_script:
-    # Avoid information carried from one run to the other
-    - rm -rf .coverage junit-*.xml .tox
-  # The following scripts are also used in Windows, be aware the shell is not POSIX
-  build_script: &build
-    - python -m tox -e build  # Ensure PEP517-style build works
-  test_script: &test
-    - python -m tox -e all -- -n 5 -rfEx --durations 10 --color yes --junit-xml junit-test.xml
-    # ^  tox is better if invoked as a module on Windows/OSX
-  always: &upload-junit
-    upload_junit_artifacts:
-      path: junit-*.xml
-      format: junit
-      type: text/xml
-  pip_cache: &pip-cache
-    folder: $PIP_CACHE
-  pre_commit_cache: &pre-commit-cache
-    folder: $PRE_COMMIT_HOME
-    fingerprint_script:
-      - echo $CIRRUS_OS
-      - python --version
-      - pre-commit --version
-      - cat .pre-commit-config.yaml src/pyscaffold/templates/pre-commit-config.template
+  pip_cache:
+    folder: "${CIRRUS_WORKING_DIR}/.cache/pip"
+    fingerprint_script: echo "${CIRRUS_OS}-${CIRRUS_TASK_NAME}"
+    reupload_on_changes: true
+  pre_commit_cache:
+    folder: "${CIRRUS_WORKING_DIR}/.cache/pre-commit"
+    fingerprint_script: echo "${CIRRUS_OS}-${CIRRUS_TASK_NAME}" | cat - .pre-commit-config.yaml
+    reupload_on_changes: true
 
+.test_template: &test-template
+  # Requires pip, tox, and pipx to be installed via OS/pip
+  alias: test
+  depends_on: [build]
+  <<: *task-template
+  dist_cache: {folder: dist, fingerprint_script: echo $CIRRUS_BUILD_ID}  # download
+  test_script: >
+    tox --installpkg dist/*.whl -e all --
+    -n 5 --randomly-seed=42 -rfEx --durations 10 --color yes
+  submit_coverage_script:
+    - pipx run coverage xml -o coverage.xml
+    - pipx run coveralls --submit coverage.xml
 
 # Deep clone script for POSIX environments (required for setuptools-scm)
 .clone_script: &clone |
@@ -60,29 +58,32 @@ env:
     git reset --hard $CIRRUS_CHANGE_IN_REPO
   fi
 
+# ---- CI Pipeline ----
 
-# ---- Task definitions ----
-
-typecheck_task:
-  name: typecheck (Linux - 3.10)
-  clone_script: *clone
-  container: {image: "python:3.10-bullseye"}  # most recent => better type support
-  pip_cache: *pip-cache
-  mypy_cache:
-    folder: .mypy_cache
-  tox_install_script:
-    - python -m pip install --upgrade pip setuptools tox
-  prepare_script: *prepare
-  clean_workspace_script:
-    - rm -rf junit-*.xml
-  typecheck_script:
-    - python -m tox -e typecheck -- src --junit-xml junit-type.xml
-  always: *upload-junit
-
-
-linux_mac_task:
-  # Use custom cloning since otherwise git tags are missing
+build_task:
+  name: build (Linux - 3.10)
+  alias: build
+  container: {image: "python:3.10-bullseye"}
   clone_script: *clone
+  dist_cache:  # build once and upload to be used by other tasks
+    folder: dist
+    fingerprint_script: echo $CIRRUS_BUILD_ID
+    reupload_on_changes: true
+  <<: *task-template
+  install_script: pip install tox
+  build_script: tox -e clean,build
+
+check_task:
+  name: check (Linux - 3.10)
+  alias: check
+  depends_on: [build]
+  container: {image: "python:3.10-bullseye"}  # most recent => better types
+  dist_cache: {folder: dist, fingerprint_script: echo $CIRRUS_BUILD_ID}  # download
+  <<: *task-template
+  install_script: pip install tox
+  check_script: tox --installpkg dist/*.whl -e lint,typecheck
+
+linux_task:
   matrix:
     - name: test (Linux - 3.6)
       container: {image: "python:3.6-bullseye"}
@@ -98,130 +99,84 @@ linux_mac_task:
     - name: test (Linux - 3.11)
       container: {image: "python:3.11-rc-bullseye"}
       allow_failures: true  # Experimental
-    - name: test (Linux - Anaconda)
-      container: {image: "continuumio/anaconda3:2021.11"}
-      env:
-        USING_CONDA: true
-      extra_install_script:
-        - conda install -y -c conda-forge virtualenv build setuptools setuptools-scm pip tox
-    - name: test (OS X)
-      macos_instance: {image: "big-sur-xcode"}
-      env:
-        PYTHON_VERSION: 3.9
-        # ^  update when the default version of python in homebrew changes
-        PATH: "${HOME}/.local/bin:${HOME}/Library/Python/${PYTHON_VERSION}/bin:/usr/local/opt/python/libexec/bin:${PATH}"
-        # ^  add user and homebrew paths
-        PIP_CACHE: "${HOME}/Library/Caches/pip"
-      brew_cache:
-        folder: "$HOME/Library/Caches/Homebrew"
-      install_script:
-        - brew install python
-        - brew cleanup
-  <<: *REGULAR_TASK_TEMPLATE
+  install_script:
+    - python -m pip install --upgrade pip tox pipx
+  <<: *test-template
+
+mamba_task:
+  name: test (Linux - mambaforge)
+  container: {image: "condaforge/mambaforge"}
+  install_script:  # Overwrite template
+    - mamba install -y pip tox pipx
+  <<: *test-template
 
+macos_task:
+  name: test (macOS - brew)
+  macos_instance: {image: "big-sur-xcode"}
+  brew_cache: {folder: "$HOME/Library/Caches/Homebrew"}
+  install_script: brew install python tox pipx
+  env:
+    PATH: "/usr/local/opt/python/libexec/bin:${PATH}"
+  <<: *test-template
+
+freebsd_task:
+  name: test (freebsd - 3.8)
+  freebsd_instance: {image_family: freebsd-13-0}
+  install_script:
+    - pkg remove -y python lang/python
+    - pkg install -y git python38 py38-pip py38-gdbm py38-sqlite3 py38-tox py38-pipx vim
+    - ln -s /usr/local/bin/python3.8 /usr/local/bin/python
+  <<: *test-template
 
 windows_task:
-  name: test (Windows)
-  tools_cache:
-    folder: 'C:\tools'
-    fingerprint_script:
-      - ps: echo "$env:CIRRUS_OS - nuget v6.0.0 - git 2.34.1"
-    populate_script:
-      - ps: (mkdir 'C:\tools')
-      # ^  use parentheses to suppress errors
-      - ps: Invoke-WebRequest -OutFile 'C:\tools\nuget.exe' 'https://dist.nuget.org/win-x86-commandline/v6.0.0/nuget.exe'
-      - ps: nuget install GitForWindows -Version 2.34.1 -NonInteractive -OutputDirectory C:\tools
-  workaround_git_script:
-    - git config --system core.longpaths true  # Fix for windows git checkout problems
-  clone_script:
-    # Use custom cloning since otherwise git tags are missing
-    CMD.exe /C ECHO ON &
-    IF NOT DEFINED CIRRUS_PR (
-      git clone --recursive --branch=%CIRRUS_BRANCH% https://x-access-token:%CIRRUS_REPO_CLONE_TOKEN%@github.com/%CIRRUS_REPO_FULL_NAME%.git %CIRRUS_WORKING_DIR% &
-      git reset --hard %CIRRUS_CHANGE_IN_REPO%
-    ) ELSE (
-      git clone --recursive https://x-access-token:%CIRRUS_REPO_CLONE_TOKEN%@github.com/%CIRRUS_REPO_FULL_NAME%.git %CIRRUS_WORKING_DIR% &
-      git fetch origin pull/%CIRRUS_PR%/head:pull/%CIRRUS_PR% &
-      git reset --hard %CIRRUS_CHANGE_IN_REPO%
-    )
+  name: test (Windows - 3.9.10)
   windows_container:
-    image: "python:3.8-windowsservercore"
+    image: "cirrusci/windowsservercore:2019"
     os_version: 2019
   env:
-    # Single quotes are used bellow to escape Windows backslash and % (YAML restrictions).
-    PYTHON_HOME: 'C:\Python'
-    PYTHON_APPDATA: '%APPDATA%\Python\Python38'
-    # ^  update when python version changes
-    GIT_HOME: 'C:\tools\GitForWindows.2.34.1\tools'
-    # ^ update when git version changes
-    HOME: '%USERPROFILE%'
-    USERNAME: ContainerAdministrator
-    # ^  ensure USERNAME is set in Windows, so the getpass module doesn't raise exceptions
-    PATH: '%HOME%\.local\bin\;%PYTHON_APPDATA%\Scripts\;%PYTHON_HOME%\;%PYTHON_HOME%\Scripts\;C:\tools\;%GIT_HOME%\cmd\;%GIT_HOME%\usr\bin\;%PATH%'
-    # ^  add user paths
-    PIP_CACHE: '%LocalAppData%\pip\Cache'
-    PIP_TRUSTED_HOST: 'pypi.org pypi.python.org files.pythonhosted.org'
-    PIP_CONFIG_FILE: '%AppData%\pip\pip.ini'
-    PRE_COMMIT_HOME: '%USERPROFILE%\.cache\pre-commit'
-    COVERAGE: 'NO'
-    # REQUESTS_CA_BUNDLE: ''
-    # CURL_CA_BUNDLE: ''
-    # ^  deactivate SSL checking to avoid error [SSL: CERTIFICATE_VERIFY_FAILED] certificate verify failed (_ssl.c:661)
-  pip_cache:
-    folder: '%PIP_CACHE%'
-  pre_commit_cache:
-    folder: '%PRE_COMMIT_HOME%'
+    CIRRUS_SHELL: bash
+    PATH: /c/Python39:/c/Python39/Scripts:/c/tools:${PATH}
   install_script:
     # Activate long file paths to avoid some errors
-    - REG ADD "HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control\FileSystem" /v LongPathsEnabled /t REG_DWORD /d 1 /f
-    # Set Windows encoding to UTF-8
-    - REG ADD "HKEY_CURRENT_USER\Software\Microsoft\Command Processor" /v Autorun /t REG_SZ /d "@chcp 65001>nul" /f
-    - python -m ensurepip
-    - python -m pip install --upgrade --user pip setuptools certifi tox pre-commit
-  prepare_script: *prepare
-  clean_workspace_script:
-    # Avoid information carried from one run to the other
-    # CMD is not capable of globbing, so we have to use PowerShell
-    - ps: (rm -Recurse -Force -ErrorAction SilentlyContinue .tox,junit-*.xml)
-  build_script: *build
-  test_script: *test
-  always: *upload-junit
-
-
-coverage_task:
-  name: coverage (Linux - 3.8)
-  clone_script: *clone
-  container: {image: "python:3.8-bullseye"}
-  env:
-    COVERAGE: yes
-  depends_on:
-    - test (Linux - 3.7)
-    - test (Linux - 3.8)
-    - test (Linux - 3.9)
-    - test (Linux - 3.10)
-    - test (Linux - Anaconda)
-    - test (OS X)
-  pip_install_script:
-    pip install --user --upgrade coverage coveralls pre-commit
-  precommit_script:
-    - pre-commit install
-    - pre-commit run --all-files
-  <<: *REGULAR_TASK_TEMPLATE
-  coverage_script:
-    - coveralls
+    - ps: New-ItemProperty -Path "HKLM:\SYSTEM\CurrentControlSet\Control\FileSystem" -Name "LongPathsEnabled" -Value 1 -PropertyType DWORD -Force
+    - choco install -y --no-progress python3 --version=3.9.10 --params "/NoLockdown"
+    - choco install -y --no-progress vim
+    - pip install --upgrade certifi
+    - python -m pip install -U pip tox pipx
+  <<: *test-template
 
+finalize_task:
+  container: {image: "python:3.10-bullseye"}
+  depends_on: [test]
+  <<: *task-template
+  install_script: pip install coveralls
+  finalize_coverage_script: coveralls --finish
 
 linkcheck_task:
-  name: linkcheck (Linux - 3.8)
-  # only_if: $CIRRUS_BRANCH == 'master'
+  name: linkcheck (Linux - 3.10)
+  container: {image: "python:3.10-bullseye"}
+  depends_on: [finalize]
   allow_failures: true
-  clone_script: *clone
-  container: {image: "python:3.8-bullseye"}
-  depends_on:
-    - test (Linux - 3.8)
-  pip_cache: *pip-cache
-  tox_install_script:
-    - python -m pip install --upgrade pip setuptools tox
-  prepare_script: *prepare
-  linkcheck_script:
-    - python -m tox -e linkcheck -- -q
+  dist_cache: {folder: dist, fingerprint_script: echo $CIRRUS_BUILD_ID}  # download
+  <<: *task-template
+  install_script: pip install tox
+  linkcheck_script: tox --installpkg dist/*.whl -e linkcheck -- -q
+
+# # The following task is already covered by a GitHub Action,
+# # (commented to avoid errors when publishing duplicated packages to PyPI)
+# publish_task:
+#   name: publish (Linux - 3.10)
+#   container: {image: "python:3.10-bullseye"}
+#   depends_on: [check, build, test]
+#   only_if: $CIRRUS_TAG =~ 'v\d.*' && $CIRRUS_USER_PERMISSION == "admin"
+#   <<: *task-template
+#   dist_cache: {folder: dist, fingerprint_script: echo $CIRRUS_BUILD_ID}  # download
+#   env:
+#     TWINE_REPOSITORY: pypi
+#     TWINE_USERNAME: __token__
+#     TWINE_PASSWORD: $PYPI_TOKEN
+#     # See: https://cirrus-ci.org/guide/writing-tasks/#encrypted-variables
+#   install_script: pip install tox
+#   publish_script:
+#     - ls dist/*
+#     - tox -e publish
diff --git a/docs/features.rst b/docs/features.rst
index 4ebcfc74c..59536f310 100644
--- a/docs/features.rst
+++ b/docs/features.rst
@@ -247,8 +247,9 @@ For usage with a continuous integration software JUnit and Coverage XML output
 can be activated in ``setup.cfg``. Use the flag ``--cirrus`` to generate
 templates of the `Cirrus CI`_ configuration file
 ``.cirrus.yml`` which even features the coverage and stats system `Coveralls`_.
-If you are using `GitLab`_ you can get a default
-`.gitlab-ci.yml` also running `pytest-cov` with the flag ``--gitlab``.
+Alternatively, you can also generate configuration files for
+`GitLab CI`_ or `GitHub Actions`_ by running ``putup`` with the
+``--gitlab`` or ``--github-actions`` flags.
 
 Managing test environments and tasks with tox
 ---------------------------------------------
@@ -325,7 +326,8 @@ PyScaffold offers several extensions:
   ``django-admin startproject my_project`` enhanced by PyScaffold's features
   (requires `pyscaffoldext-django`_).
 
-* … and many more like ``--gitlab`` to create the necessary files for GitLab_,
+* … and many more like ``--gitlab`` to create the necessary files for `GitLab CI`_,
+  ``--github-actions`` to configure `GitHub Actions`_,
   ``--travis`` for `Travis CI`_ (see `pyscaffoldext-travis`_), or
   ``--cookiecutter`` for Cookiecutter_ integration (see `pyscaffoldext-cookiecutter`_).
 
@@ -412,7 +414,8 @@ Check out our :ref:`Configuration <default-cfg>` section to get started.
 .. _importlib.resources: https://docs.python.org/3/library/importlib.html#module-importlib.resources
 .. _importlib_resources: https://importlib-resources.readthedocs.io/en/stable/
 .. _flake8: https://flake8.pycqa.org/en/stable/
-.. _GitLab: https://about.gitlab.com/
+.. _GitLab CI: https://docs.gitlab.com/ee/ci/
+.. _GitHub Actions: https://github.com/features/actions
 .. _PEP 420: https://www.python.org/dev/peps/pep-0420/
 .. _PEP 440: https://www.python.org/dev/peps/pep-0440/
 .. _PEP 517: https://www.python.org/dev/peps/pep-0517/
diff --git a/setup.cfg b/setup.cfg
index 9ef5e9831..0c339ea03 100644
--- a/setup.cfg
+++ b/setup.cfg
@@ -95,6 +95,7 @@ testing =
     pytest-virtualenv
     pytest-fixture-config
     pytest-xdist
+    pytest-randomly
     # We keep pytest-xdist in the test dependencies, so the developer can
     # easily opt-in for distributed tests by adding, for example, the `-n 15`
     # arguments in the command-line.
diff --git a/src/pyscaffold/templates/cirrus.template b/src/pyscaffold/templates/cirrus.template
index cdcddee75..547a4d45e 100644
--- a/src/pyscaffold/templates/cirrus.template
+++ b/src/pyscaffold/templates/cirrus.template
@@ -1,46 +1,60 @@
 # Cirrus CI configuration file using the matrix feature
-# Read more under https://cirrus-ci.org/guide/writing-tasks/
+# Read more under:
+# - https://cirrus-ci.org/guide/quick-start/
+# - https://cirrus-ci.org/guide/writing-tasks/
 # THIS FILE IS SUPPOSED TO BE AN EXAMPLE. MODIFY IT ACCORDING TO YOUR NEEDS!
+
 ---
-# Default values to be merged into tasks:
-auto_cancellation: false
+# ---- Default values to be merged into tasks ----
+
 env:
-  PATH: ${HOME}/.local/bin:${PATH}
-  # ^  add user paths
-  # COVERALLS_REPO_TOKEN: ENCRYPTED[]
-  # ^ You can add an encrypted token or use CirrusCI web interface to set the variable
-  #   See discussion in: https://github.com/cirruslabs/cirrus-ci-docs/discussions/866
-  PIP_CACHE: ${HOME}/.cache/pip
   LC_ALL: C.UTF-8
   LANG: C.UTF-8
-  COVERAGE: NO
+  PIP_CACHE_DIR: ${CIRRUS_WORKING_DIR}/.cache/pip
+  PRE_COMMIT_HOME: ${CIRRUS_WORKING_DIR}/.cache/pre-commit
+  # Coveralls configuration
   CI_NAME: cirrus-ci
   CI_BRANCH: ${CIRRUS_BRANCH}
   CI_PULL_REQUEST: ${CIRRUS_PR}
   CI_BUILD_NUMBER: ${CIRRUS_BUILD_ID}
   CI_BUILD_URL: https://cirrus-ci.com/build/${CIRRUS_BUILD_ID}
+  COVERALLS_PARALLEL: "true"
+  COVERALLS_FLAG_NAME: ${CIRRUS_TASK_NAME}
+  # TODO: You will also need to set `COVERALLS_REPO_TOKEN` to work with coveralls.
+  #       We recommend that you do that via Cirrus CI web interface via
+  #       `Environment variable overrides`.
+  # See: https://coveralls-python.readthedocs.io/en/latest/usage/index.html
 
+# ---- Templates ----
 
-# This template is used in most of the tasks
-.regular_task_template: &REGULAR_TASK_TEMPLATE
-  pip_cache: &pip-cache
-    folder: $PIP_CACHE
-  tox_install_script:
-    - python -m pip install --upgrade pip setuptools tox
-  prepare_script: &prepare
-    # This script is also used in Windows, so the shell is not POSIX
+.task_template: &task-template
+  debug_information_script:
+    - echo "$(which python) -- $(python -VV)"
+    - echo "$(which pip) -- $(pip -VV)"
+    - python -c 'import os, sys; print(os.name, sys.platform, getattr(sys, "abiflags", None))'
+  prepare_script:  # avoid git failing with setuptools-scm
     - git config --global user.email "you@example.com"
     - git config --global user.name "Your Name"
-  clean_workspace_script:
-    # Avoid information carried from one run to the other
-    - rm -rf .coverage junit-*.xml .tox
-  # The following scripts are also used in Windows, be aware the shell is not POSIX
-  build_script: &build
-    - python -m tox -e build  # Ensure PEP517-style build works
-  test_script: &test
-    - python -m tox -- -rfEx --durations 10 --color yes
-    # ^  tox is better if invoked as a module on Windows/OSX
+  pip_cache:
+    folder: "${CIRRUS_WORKING_DIR}/.cache/pip"
+    fingerprint_script: echo "${CIRRUS_OS}-${CIRRUS_TASK_NAME}"
+    reupload_on_changes: true
+  pre_commit_cache:
+    folder: "${CIRRUS_WORKING_DIR}/.cache/pre-commit"
+    fingerprint_script: echo "${CIRRUS_OS}-${CIRRUS_TASK_NAME}" | cat - .pre-commit-config.yaml
+    reupload_on_changes: true
 
+.test_template: &test-template
+  # Requires pip, tox, and pipx to be installed via OS/pip
+  alias: test
+  depends_on: [build]
+  <<: *task-template
+  dist_cache: {folder: dist, fingerprint_script: echo $CIRRUS_BUILD_ID}  # download
+  test_script:
+    - tox --installpkg dist/*.whl -- -rfEx --durations 10 --color yes
+  submit_coverage_script:
+    - pipx run coverage xml -o coverage.xml
+    - pipx run coveralls --submit coverage.xml
 
 # Deep clone script for POSIX environments (required for setuptools-scm)
 .clone_script: &clone |
@@ -53,12 +67,34 @@ env:
     git reset --hard $CIRRUS_CHANGE_IN_REPO
   fi
 
+# ---- CI Pipeline ----
 
-# ---- Task definitions ----
-
-linux_mac_task:
-  # Use custom cloning since otherwise git tags are missing
+build_task:
+  name: build (Linux - 3.10)
+  alias: build
+  container: {image: "python:3.10-bullseye"}
   clone_script: *clone
+  dist_cache:  # build once and upload to be used by other tasks
+    folder: dist
+    fingerprint_script: echo $CIRRUS_BUILD_ID
+    reupload_on_changes: true
+  <<: *task-template
+  install_script: pip install tox
+  build_script: tox -e clean,build
+
+check_task:
+  name: check (Linux - 3.10)
+  alias: check
+  depends_on: [build]
+  container: {image: "python:3.10-bullseye"}  # most recent => better types
+  dist_cache: {folder: dist, fingerprint_script: echo $CIRRUS_BUILD_ID}  # download
+  <<: *task-template
+  install_script: pip install pre-commit
+  check_script:
+    - pre-commit run --all-files --show-diff-on-failure --color always
+    # - tox -e typecheck
+
+linux_task:
   matrix:
     - name: test (Linux - 3.7)
       container: {image: "python:3.7-bullseye"}
@@ -68,115 +104,88 @@ linux_mac_task:
       container: {image: "python:3.9-bullseye"}
     - name: test (Linux - 3.10)
       container: {image: "python:3.10-bullseye"}
-    - name: test (Linux - Anaconda)
-      container: {image: "continuumio/anaconda3:2021.11"}
-      extra_install_script:
-        - conda install -y -c conda-forge virtualenv build setuptools setuptools-scm pip tox
-    - name: test (OS X)
-      macos_instance: {image: "big-sur-xcode"}
-      env:
-        PYTHON_VERSION: 3.9
-        # ^  update when the default version of python in homebrew changes
-        PATH: "${HOME}/.local/bin:${HOME}/Library/Python/${PYTHON_VERSION}/bin:/usr/local/opt/python/libexec/bin:${PATH}"
-        # ^  add user and homebrew paths
-        PIP_CACHE: "${HOME}/Library/Caches/pip"
-      brew_cache:
-        folder: "$HOME/Library/Caches/Homebrew"
-      install_script:
-        - brew install python
-        - brew cleanup
-  <<: *REGULAR_TASK_TEMPLATE
-
-
-# If you find some problems with long paths on Windows,
-# please check the .cirrus.yml file of PyScaffold itself for some workarounds.
+    - name: test (Linux - 3.11)
+      container: {image: "python:3.11-rc-bullseye"}
+      allow_failures: true  # Experimental
+  install_script:
+    - python -m pip install --upgrade pip tox pipx
+  <<: *test-template
+
+mamba_task:
+  name: test (Linux - mambaforge)
+  container: {image: "condaforge/mambaforge"}
+  install_script:  # Overwrite template
+    - mamba install -y pip tox pipx
+  <<: *test-template
+
+macos_task:
+  name: test (macOS - brew)
+  macos_instance: {image: "big-sur-xcode"}
+  brew_cache: {folder: "$HOME/Library/Caches/Homebrew"}
+  install_script: brew install python tox pipx
+  env:
+    PATH: "/usr/local/opt/python/libexec/bin:${PATH}"
+  <<: *test-template
+
+freebsd_task:
+  name: test (freebsd - 3.8)
+  freebsd_instance: {image_family: freebsd-13-0}
+  install_script:
+    - pkg remove -y python lang/python
+    - pkg install -y git python38 py38-pip py38-gdbm py38-sqlite3 py38-tox py38-pipx
+    - ln -s /usr/local/bin/python3.8 /usr/local/bin/python
+  <<: *test-template
+
 windows_task:
-  name: test (Windows)
-  tools_cache:
-    folder: 'C:\tools'
-    fingerprint_script:
-      - ps: echo "$env:CIRRUS_OS - nuget v6.0.0 - git 2.34.1"
-    populate_script:
-      - ps: (mkdir 'C:\tools')
-      # ^  use parentheses to suppress errors
-      - ps: Invoke-WebRequest -OutFile 'C:\tools\nuget.exe' 'https://dist.nuget.org/win-x86-commandline/v6.0.0/nuget.exe'
-      - ps: nuget install GitForWindows -Version 2.34.1 -NonInteractive -OutputDirectory C:\tools
-  workaround_git_script:
-    - git config --system core.longpaths true  # Fix for windows git checkout problems
-  clone_script:
-    # Use custom cloning since otherwise git tags are missing
-    CMD.exe /C ECHO ON &
-    IF NOT DEFINED CIRRUS_PR (
-      git clone --recursive --branch=%CIRRUS_BRANCH% https://x-access-token:%CIRRUS_REPO_CLONE_TOKEN%@github.com/%CIRRUS_REPO_FULL_NAME%.git %CIRRUS_WORKING_DIR% &
-      git reset --hard %CIRRUS_CHANGE_IN_REPO%
-    ) ELSE (
-      git clone --recursive https://x-access-token:%CIRRUS_REPO_CLONE_TOKEN%@github.com/%CIRRUS_REPO_FULL_NAME%.git %CIRRUS_WORKING_DIR% &
-      git fetch origin pull/%CIRRUS_PR%/head:pull/%CIRRUS_PR% &
-      git reset --hard %CIRRUS_CHANGE_IN_REPO%
-    )
+  name: test (Windows - 3.9.10)
   windows_container:
-    image: "python:3.8-windowsservercore"
+    image: "cirrusci/windowsservercore:2019"
     os_version: 2019
   env:
-    # Single quotes are used bellow to escape Windows backslash and % (YAML restrictions).
-    PYTHON_HOME: 'C:\Python'
-    PYTHON_APPDATA: '%APPDATA%\Python\Python38'
-    # ^  update when python version changes
-    GIT_HOME: 'C:\tools\GitForWindows.2.34.1\tools'
-    # ^ update when git version changes
-    HOME: '%USERPROFILE%'
-    USERNAME: ContainerAdministrator
-    # ^  ensure USERNAME is set in Windows, so the getpass module doesn't raise exceptions
-    PATH: '%HOME%\.local\bin\;%PYTHON_APPDATA%\Scripts\;%PYTHON_HOME%\;%PYTHON_HOME%\Scripts\;C:\tools\;%GIT_HOME%\cmd\;%PATH%'
-    # ^  add user paths (if POSIX tools are needed you can try also adding %GIT_HOME\usr\bin\)
-    PIP_CACHE: '%LocalAppData%\pip\Cache'
-    PIP_TRUSTED_HOST: 'pypi.org pypi.python.org files.pythonhosted.org'
-    PIP_CONFIG_FILE: '%AppData%\pip\pip.ini'
-    COVERAGE: 'NO'
-  pip_cache:
-    folder: '%PIP_CACHE%'
+    CIRRUS_SHELL: bash
+    PATH: /c/Python39:/c/Python39/Scripts:${PATH}
   install_script:
     # Activate long file paths to avoid some errors
-    - REG ADD "HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control\FileSystem" /v LongPathsEnabled /t REG_DWORD /d 1 /f
-    # Set Windows encoding to UTF-8
-    - REG ADD "HKEY_CURRENT_USER\Software\Microsoft\Command Processor" /v Autorun /t REG_SZ /d "@chcp 65001>nul" /f
-    - python -m ensurepip
-    - python -m pip install --upgrade --user pip setuptools certifi tox
-  prepare_script: *prepare
-  clean_workspace_script:
-    # Avoid information carried from one run to the other
-    # CMD is not capable of globbing, so we have to use PowerShell
-    - ps: (rm -Recurse -Force -ErrorAction SilentlyContinue .tox,junit-*.xml)
-  build_script: *build
-  test_script: *test
-
-
-coverage_task:
-  name: coverage (Linux - 3.8)
-  clone_script: *clone
-  container: {image: "python:3.8-bullseye"}
+    - ps: New-ItemProperty -Path "HKLM:\SYSTEM\CurrentControlSet\Control\FileSystem" -Name "LongPathsEnabled" -Value 1 -PropertyType DWORD -Force
+    - choco install -y --no-progress python3 --version=3.9.10 --params "/NoLockdown"
+    - pip install --upgrade certifi
+    - python -m pip install -U pip tox pipx
+  <<: *test-template
+
+finalize_task:
+  container: {image: "python:3.10-bullseye"}
+  depends_on: [test]
+  <<: *task-template
+  install_script: pip install coveralls
+  finalize_coverage_script: coveralls --finish
+
+linkcheck_task:
+  name: linkcheck (Linux - 3.10)
+  container: {image: "python:3.10-bullseye"}
+  depends_on: [finalize]
+  allow_failures: true
+  dist_cache: {folder: dist, fingerprint_script: echo $CIRRUS_BUILD_ID}  # download
+  <<: *task-template
+  install_script: pip install tox
+  linkcheck_script: tox --installpkg dist/*.whl -e linkcheck -- -q
+
+publish_task:
+  name: publish (Linux - 3.10)
+  container: {image: "python:3.10-bullseye"}
+  depends_on: [check, build, test]
+  only_if: $CIRRUS_TAG =~ 'v\d.*' && $CIRRUS_USER_PERMISSION == "admin"
+  <<: *task-template
+  dist_cache: {folder: dist, fingerprint_script: echo $CIRRUS_BUILD_ID}  # download
   env:
-    COVERAGE: yes
-    PRE_COMMIT_HOME: ${HOME}/.cache/pre-commit
-  depends_on:
-    - test (Linux - 3.7)
-    - test (Linux - 3.8)
-    - test (Linux - 3.9)
-    - test (Linux - 3.10)
-    - test (Linux - Anaconda)
-    - test (OS X)
-  pip_install_script:
-    pip install --user --upgrade coverage coveralls pre-commit
-  precommit_script:
-    - pre-commit install
-    - pre-commit run --all-files
-  <<: *REGULAR_TASK_TEMPLATE
-  coverage_script:
-    - coveralls
-  pre_commit_cache: &pre-commit-cache
-    folder: $PRE_COMMIT_HOME
-    fingerprint_script:
-      - echo $CIRRUS_OS
-      - python --version
-      - pre-commit --version
-      - cat .pre-commit-config.yaml
+    TWINE_REPOSITORY: pypi
+    TWINE_USERNAME: __token__
+    TWINE_PASSWORD: $PYPI_TOKEN
+    # TODO: You can set a `environment variable override` for PYPI_TOKEN
+    #       in Cirrus CI web interface using an encrypted variable.
+    # See:
+    # - https://pypi.org/help/#apitoken
+    # - https://cirrus-ci.org/guide/writing-tasks/#encrypted-variables
+  install_script: pip install tox
+  publish_script:
+    - ls dist/*
+    - tox -e publish
diff --git a/src/pyscaffold/templates/github_ci_workflow.template b/src/pyscaffold/templates/github_ci_workflow.template
index c0cdfe978..f9f46194b 100644
--- a/src/pyscaffold/templates/github_ci_workflow.template
+++ b/src/pyscaffold/templates/github_ci_workflow.template
@@ -107,7 +107,9 @@ jobs:
         with: {name: python-distribution-files, path: dist/}
       - name: Publish Package
         env:
-          # See: https://pypi.org/help/#apitoken
+          # TODO: Set your PYPI_TOKEN as a secret using GitHub UI
+          # - https://pypi.org/help/#apitoken
+          # - https://docs.github.com/en/actions/security-guides/encrypted-secrets
           TWINE_REPOSITORY: pypi
           TWINE_USERNAME: __token__
           TWINE_PASSWORD: ${{ secrets.PYPI_TOKEN }}
diff --git a/src/pyscaffold/templates/gitlab_ci.template b/src/pyscaffold/templates/gitlab_ci.template
index d438e5673..fcdeec2cc 100644
--- a/src/pyscaffold/templates/gitlab_ci.template
+++ b/src/pyscaffold/templates/gitlab_ci.template
@@ -16,6 +16,10 @@ variables:
   CI_BRANCH: $CI_COMMIT_REF_NAME
   CI_BUILD_NUMBER: $CI_PIPELINE_ID
   CI_BUILD_URL: $CI_PIPELINE_URL
+  # TODO: You will also need to set `COVERALLS_REPO_TOKEN` to work with coveralls.
+  #       We recommend that you do that via GitLab CI web interface.
+  # - https://coveralls-python.readthedocs.io/en/latest/usage/index.html
+  # - https://docs.gitlab.com/ee/ci/variables/
 
 workflow:
   rules:
diff --git a/src/pyscaffold/templates/tox_ini.template b/src/pyscaffold/templates/tox_ini.template
index de5939185..d7fd9d3fe 100644
--- a/src/pyscaffold/templates/tox_ini.template
+++ b/src/pyscaffold/templates/tox_ini.template
@@ -3,7 +3,7 @@
 # THIS SCRIPT IS SUPPOSED TO BE AN EXAMPLE. MODIFY IT ACCORDING TO YOUR NEEDS!
 
 [tox]
-minversion = 3.15
+minversion = 3.24
 envlist = default
 isolated_build = ${isolated_build}
 
diff --git a/tox.ini b/tox.ini
index 5bfdc23d2..1422ae7b5 100644
--- a/tox.ini
+++ b/tox.ini
@@ -2,7 +2,7 @@
 # Use `setup.cfg` as single-source of truth for dependency specification
 
 [tox]
-minversion = 3.15
+minversion = 3.24
 envlist = default
 isolated_build = True
 
@@ -50,6 +50,7 @@ deps = pre-commit
 passenv =
     HOMEPATH
     PROGRAMDATA
+    TERM
 commands =
     pre-commit run --all-files {posargs:--show-diff-on-failure}
 
@@ -101,7 +102,7 @@ passenv =
 deps = twine
 commands =
     python -m twine check dist/*
-    python -m twine upload {posargs:--repository testpypi} dist/*
+    python -m twine upload {posargs:--repository {env:TWINE_REPOSITORY:testpypi}} dist/*
 
 
 [testenv:typecheck]


Test patch:
diff --git a/tests/test_dependencies.py b/tests/test_dependencies.py
index a40027bb2..544f106d1 100644
--- a/tests/test_dependencies.py
+++ b/tests/test_dependencies.py
@@ -1,6 +1,14 @@
+import sys
+
+import pytest
+
 from pyscaffold import dependencies as deps
 
 
+@pytest.mark.skipif(
+    sys.version_info[:2] <= (3, 7),
+    reason="regex doesn't work well in py36 (for some edge cases)",
+)
 def test_split():
     assert deps.split(
         "\n    pyscaffold>=42.1.0,<43.0"
diff --git a/tests/test_update.py b/tests/test_update.py
index 5305386a3..5013e5b06 100644
--- a/tests/test_update.py
+++ b/tests/test_update.py
@@ -28,7 +28,7 @@ def __init__(self, tmpdir, venv, pytestconfig):
         self.install("coverage")
         self.running_version = Version(__version__)
 
-    def install(self, pkg=None, editable=False):
+    def install(self, pkg=None, editable=False, **kwargs):
         pkg = f'"{pkg}"'  # Windows requires double quotes to work properly with ranges
         if editable:
             pkg = f"--editable {pkg}"
@@ -40,7 +40,8 @@ def install(self, pkg=None, editable=False):
         # var for trusted hosts
         return self.run(
             f"{python} -m pip install {pkg} --trusted-host pypi.python.org "
-            "--trusted-host files.pythonhosted.org --trusted-host pypi.org"
+            "--trusted-host files.pythonhosted.org --trusted-host pypi.org",
+            **kwargs,
         )
 
     def install_this_pyscaffold(self):
@@ -62,8 +63,9 @@ def install_this_pyscaffold(self):
 
             logging.debug("SRC via working_set: %s, location: %s", proj_dir, location)
 
+        env = {**os.environ, "SETUPTOOLS_SCM_PRETEND_VERSION": __version__}
         assert proj_dir.exists(), f"{proj_dir} is supposed to exist"
-        self.install(proj_dir, editable=True)
+        self.install(proj_dir, editable=True, env=env)
         # Make sure pyscaffold was not installed using PyPI
         assert self.running_version.public <= self.pyscaffold_version().public
         pkg_list = self.run(f"{self.venv.python} -m pip freeze")