diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md new file mode 100644 index 000000000..251219108 --- /dev/null +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -0,0 +1,8 @@ +### What kind of change does this PR introduce? + +* ... + +### Does this PR introduce a breaking change? + + +### Other information: diff --git a/README.rst b/README.rst index 0d27ef16d..26c600885 100644 --- a/README.rst +++ b/README.rst @@ -2,22 +2,12 @@ Cookiecutter PyPackage ====================== -.. image:: https://pyup.io/repos/github/audreyfeldroy/cookiecutter-pypackage/shield.svg - :target: https://pyup.io/repos/github/audreyfeldroy/cookiecutter-pypackage/ - :alt: Updates - -.. image:: https://github.com/Ouranosinc/cookiecutter-pypackage/actions/workflows/main.yml/badge.svg - :target: https://github.com/Ouranosinc/cookiecutter-pypackage/actions/workflows/main.yml - :alt: Build Status - -.. image:: https://readthedocs.org/projects/cookiecutter-pypackage/badge/?version=latest - :target: https://cookiecutter-pypackage.readthedocs.io/en/latest/?badge=latest - :alt: Documentation Status +|build| Cookiecutter_ template for a Python package. * GitHub repo (fork): https://github.com/Ouranosinc/cookiecutter-pypackage/ -* Documentation: https://cookiecutter-pypackage.readthedocs.io/ +* Documentation (upstream): https://cookiecutter-pypackage.readthedocs.io/ * Free software: BSD license Features @@ -29,29 +19,30 @@ Features * `Conda`_ environment file: Optionally use ``conda env create -f environment-dev.yml`` to create a new environment with the correct Python version. * Tox_ testing: Setup to easily test for Python 3.8, 3.9, 3.10, 3.11, 3.12, and PyPy3. * Sphinx_ docs: Documentation ready for generation with, for example, `Read the Docs`_ -* pre-commit_ hook: Run your tests and linting (e.g. Flake8, Black) before you commit your code! -* bump-my-version_: Pre-configured version bumping with a single command +* pre-commit_ hook: Run your tests and linting (e.g. `black`, `flake8`, `pylint`, etc.) before you commit your code! +* `pre-commit.ci`_: Automate `pre-commit` checks and corrections in your Pull Requests. +* bump-my-version_: Pre-configured `SemVer-2.0-compliant`_ version bumping with a single command. +* dependabot_ for automated dependency updates of both project dependencies and GitHub Actions. * `sphinx-intl`_ for French internationalization (i18n) and localization (l10n) of Sphinx docs (optional) -* Auto-release to PyPI_ when you push a new tag to main (optional) +* Auto-release to TestPyPI_ and PyPI_ when you push a new tag to main (optional) * Command line interface using Click (optional) Build Status ------------- -Linux: - -.. image:: https://github.com/Ouranosinc/cookiecutter-pypackage/actions/workflows/main.yml/badge.svg - :target: https://github.com/Ouranosinc/cookiecutter-pypackage/actions/workflows/main.yml - :alt: Linux build status on GitHub Actions +Upstream (audreyfeldroy/cookiecutter-pypackage): |docs-upstream| |pyup-upstream| Quickstart ---------- -Install the latest Cookiecutter if you haven't installed it yet (this requires -Cookiecutter 1.4.0 or higher):: +Install the latest Cookiecutter if you haven't installed it yet :: $ pip install -U cookiecutter +Or, if using Conda:: + + $ conda install -c conda-forge cookiecutter + Generate a Python package project:: $ cookiecutter https://github.com/Ouranosinc/cookiecutter-pypackage.git @@ -59,21 +50,62 @@ Generate a Python package project:: Then: * Create a repo and put it there. -* Install the dev requirements into a virtualenv. (``pip install -r requirements_dev.txt``) -* Register_ your project with PyPI. -* Enable GitHub Actions and Workflows and activate automated deployment on PyPI when you push a new tag to main branch. -* Add the repo to your `Read the Docs`_ account + turn on the Read the Docs service hook. -* Release your package by pushing a new tag to main. -* Add a ``requirements.txt`` file that specifies the packages you will need for - your project and their versions. For more info see the `pip docs for requirements files`_. -* Activate your project on `pyup.io`_. - -.. _`pip docs for requirements files`: https://pip.pypa.io/en/stable/user_guide/#requirements-files -.. _Register: https://packaging.python.org/tutorials/packaging-projects/#uploading-the-distribution-archives +* Install the development requirements into an environment:: + + $ pip install -e ".[dev]" + +Or, if using Conda:: + + $ conda env create -f environment-dev.yml + $ pip install -e . --no-deps + +* `Register your project with PyPI `_. +* Enable GitHub Actions and Workflows (see below). +* Activate automated deployment with `Trusted Publishing`_ to PyPI when you push a new tag to the `main` branch. +* Add the repo to your `Read the Docs`_ account and turn on the ReadTheDocs service hook. +* Release your package by pushing a new tag to `main`. +* Update the `dependencies` field of your `pyproject.toml` file that specifies the packages you will need for + your project and their versions. For more info see the `pip docs for requirements files `_. +* Register your project with `pre-commit.ci`_. +* Activate `dependabot`_ for your project. For more details, see the `cookiecutter-pypackage tutorial`_. -.. _`cookiecutter-pypackage tutorial`: https://cookiecutter-pypackage.readthedocs.io/en/latest/tutorial.html +GitHub Actions +~~~~~~~~~~~~~~ + +In order to use GitHub Actions, you will need to enable them in your repo. To do so, go to the `Actions` tab of your repo and click the green button to enable them. Afterwards, you will need to ~generate a few Personal Access Tokens (PATs) `_ to allow the workflows to run. To do so, go to the `Settings` tab of your repo and click on `Secrets` in the left sidebar. Then, click on the `New repository secret` button and add the following secrets: + +* `BUMP_VERSION_TOKEN` with the following privileges: + - Contents: Read and Write + - Metadata: Read-Only + - Pull Requests: Read and Write + +* `OPENSSF_SCORECARD_TOKEN` with the following privileges: + - Administration: Read-Only + - Metadata: Read-Only + - Webhooks: Read-Only + +Trusted Publishing +~~~~~~~~~~~~~~~~~~ + +For Trusted Publishing with PyPI_ and TestPyPI_, you will need to create deployment environments in your repo. To do so, go to the `Settings` tab of your repo and click on `Environments` in the left sidebar. Then, click on the `New environment` button and add the following environments: + +* `staging` +* `production` + +Afterwards, you will need to configure your project on both PyPI_ and TestPyPI_ to accept uploads from GitHub Actions. To do so, go to the `Manage` tab of your project on PyPI and click on `Publishing` in the left sidebar. Then, click on the `Add a new publisher` button and fill in the following information: + +* Owner: `my_username` +* Repository name: `my_project` +* Workflow name: + * For TestPyPI: `tag-testpypi.yml` + * For PyPI: `publish-pypi.yml` +* Environment name: + * For TestPyPI: `staging` + * For PyPI: `production` + +Once this is configured, all you need to do is push a new tag to the `main` branch and your package will be automatically published to TestPyPI_, while performing a release on GitHub will then trigger an upload to PyPI_. Not Exactly What You Want? -------------------------- @@ -128,27 +160,44 @@ I also accept pull requests on this, if they're small, atomic, and if they make .. _Mkdocs: https://pypi.org/project/mkdocs/ .. _Mypy: https://mypy.readthedocs.io/en/stable/ .. _Poetry: https://python-poetry.org/ -.. _Pre-commit: https://pre-commit.com/ .. _Punch: https://github.com/lgiordani/punch -.. _PyPI: https://pypi.python.org/pypi .. _Read the Docs: https://readthedocs.io/ +.. _SemVer-2.0-compliant: https://semver.org/spec/v2.0.0.html .. _Sphinx: http://sphinx-doc.org/ .. _Tox: http://testrun.org/tox/ -.. _`pyproject.toml`: https://www.python.org/dev/peps/pep-0518/ -.. _`pyup.io`: https://pyup.io/ -.. _bump2version: https://github.com/c4urself/bump2version .. _bump-my-version: https://github.com/callowayproject/bump-my-version +.. _bump2version: https://github.com/c4urself/bump2version +.. _cookiecutter-pypackage tutorial: https://cookiecutter-pypackage.readthedocs.io/en/latest/tutorial.html +.. _dependabot: https://docs.github.com/en/code-security/dependabot/dependabot-version-updates .. _flit: https://flit.pypa.io/en/stable/ +.. _pre-commit.ci: https://pre-commit.ci/ +.. _pre-commit: https://pre-commit.com/ +.. _pypi: https://pypi.org/ +.. _pyproject.toml: https://www.python.org/dev/peps/pep-0518/ +.. _pyup.io: https://pyup.io/ .. _sphinx-intl: https://sphinx-intl.readthedocs.io/en/master/ +.. _testpypi: https://test.pypi.org/ .. _GitHub comparison view: https://github.com/tony/cookiecutter-pypackage-pythonic/compare/audreyr:master...master -.. _`Nekroze/cookiecutter-pypackage`: https://github.com/Nekroze/cookiecutter-pypackage -.. _`ardydedase/cookiecutter-pypackage`: https://github.com/ardydedase/cookiecutter-pypackage -.. _`briggySmalls/cookiecutter-pypackage`: https://github.com/briggySmalls/cookiecutter-pypackage -.. _`family tree`: https://github.com/audreyr/cookiecutter-pypackage/network/members -.. _`lgiordani/cookiecutter-pypackage`: https://github.com/lgiordani/cookiecutter-pypackage -.. _`tony/cookiecutter-pypackage-pythonic`: https://github.com/tony/cookiecutter-pypackage-pythonic -.. _`veit/cookiecutter-namespace-template`: https://github.com/veit/cookiecutter-namespace-template -.. _`waynerv/cookiecutter-pypackage`: https://waynerv.github.io/cookiecutter-pypackage/ -.. _`zillionare/cookiecutter-pypackage`: https://zillionare.github.io/cookiecutter-pypackage/ +.. _Nekroze/cookiecutter-pypackage: https://github.com/Nekroze/cookiecutter-pypackage +.. _ardydedase/cookiecutter-pypackage: https://github.com/ardydedase/cookiecutter-pypackage +.. _briggySmalls/cookiecutter-pypackage: https://github.com/briggySmalls/cookiecutter-pypackage +.. _family tree: https://github.com/audreyr/cookiecutter-pypackage/network/members +.. _lgiordani/cookiecutter-pypackage: https://github.com/lgiordani/cookiecutter-pypackage .. _network: https://github.com/audreyr/cookiecutter-pypackage/network +.. _tony/cookiecutter-pypackage-pythonic: https://github.com/tony/cookiecutter-pypackage-pythonic +.. _veit/cookiecutter-namespace-template: https://github.com/veit/cookiecutter-namespace-template +.. _waynerv/cookiecutter-pypackage: https://waynerv.github.io/cookiecutter-pypackage/ +.. _zillionare/cookiecutter-pypackage: https://zillionare.github.io/cookiecutter-pypackage/ + +.. |build| image:: https://github.com/Ouranosinc/cookiecutter-pypackage/actions/workflows/main.yml/badge.svg + :target: https://github.com/Ouranosinc/cookiecutter-pypackage/actions/workflows/main.yml + :alt: Build Status + +.. |docs-upstream| image:: https://readthedocs.org/projects/cookiecutter-pypackage/badge/?version=latest + :target: https://cookiecutter-pypackage.readthedocs.io/en/latest/?badge=latest + :alt: Documentation Status + +.. |pyup-upstream| image:: https://pyup.io/repos/github/audreyfeldroy/cookiecutter-pypackage/shield.svg + :target: https://pyup.io/repos/github/audreyfeldroy/cookiecutter-pypackage/ + :alt: Updates diff --git a/hooks/post_gen_project.py b/hooks/post_gen_project.py index 2360deafd..69079d2ba 100644 --- a/hooks/post_gen_project.py +++ b/hooks/post_gen_project.py @@ -23,12 +23,13 @@ def remove_folder(folder_path): def replace_contents(filepath): replacements = { - "__BUMPVERSION_TOKEN__": "secrets.BUMPVERSION_TOKEN", + "__BUMP_VERSION_TOKEN__": "secrets.BUMP_VERSION_TOKEN", "__GITHUB_REF__": "github.ref", "__GITHUB_REF_NAME__": "github.ref_name", "__GITHUB_TOKEN__": "secrets.GITHUB_TOKEN", "__PYTHON_VERSION__": "matrix.python-version", "__TOX_ENV__": "matrix.tox-env", + "__OPENSSF_SCORECARD_TOKEN__": "secrets.OPENSSF_SCORECARD_TOKEN", } lines = [] diff --git a/tests/test_bake_project.py b/tests/test_bake_project.py index 51288f7fc..556da29f3 100644 --- a/tests/test_bake_project.py +++ b/tests/test_bake_project.py @@ -135,6 +135,7 @@ def test_bake_with_apostrophe_and_run_tests(cookies): assert result.project_path.is_dir() assert run_inside_dir("python -m coverage", str(result.project_path)) == 0 + def test_bake_without_translations(cookies): with bake_in_temp_dir(cookies, extra_context={"add_translations": "n"}) as result: found_toplevel_files = [f.name for f in result.project_path.iterdir()] @@ -146,6 +147,7 @@ def test_bake_without_translations(cookies): with open(str(pyproject_path)) as pyproject_file: assert "sphinx-intl" not in pyproject_file.read() + def test_bake_without_docs(cookies): with bake_in_temp_dir(cookies, extra_context={"make_docs": "n"}) as result: found_toplevel_files = [f.name for f in result.project_path.iterdir()] @@ -333,8 +335,8 @@ def test_black(cookies, use_black, expected): with bake_in_temp_dir(cookies, extra_context={"use_black": use_black}) as result: assert result.project_path.is_dir() requirements_path = result.project_path.joinpath("pyproject.toml") - assert ("black>=" in requirements_path.read_text()) is expected - assert ("isort>=" in requirements_path.read_text()) is expected + assert ("black==" in requirements_path.read_text()) is expected + assert ("isort==" in requirements_path.read_text()) is expected assert ("[tool.black]" in requirements_path.read_text()) is expected makefile_path = result.project_path.joinpath("Makefile") assert ("black --check" in makefile_path.read_text()) is expected diff --git a/{{cookiecutter.project_slug}}/.github/dependabot.yml b/{{cookiecutter.project_slug}}/.github/dependabot.yml new file mode 100644 index 000000000..a9f287548 --- /dev/null +++ b/{{cookiecutter.project_slug}}/.github/dependabot.yml @@ -0,0 +1,15 @@ +version: 2 +updates: + - package-ecosystem: github-actions + directory: / + schedule: + interval: daily + time: '12:00' + open-pull-requests-limit: 10 + + - package-ecosystem: pip + directory: / + schedule: + interval: daily + time: '12:00' + open-pull-requests-limit: 10 diff --git a/{{cookiecutter.project_slug}}/.github/labeler.yml b/{{cookiecutter.project_slug}}/.github/labeler.yml new file mode 100644 index 000000000..9ca3ff219 --- /dev/null +++ b/{{cookiecutter.project_slug}}/.github/labeler.yml @@ -0,0 +1,49 @@ +# label rules used by .github/workflows/label.yml + +# label 'ci' all automation-related steps and files +# Since this repository is in itself an automation process to deploy a server instance, +# we refer here to CI as the 'meta' configuration files for managing the code and integrations with the repository, +# not configurations related to the deployment process itself. + +# Uncomment the following lines to enable the labeler (requires labels with the same name to exist in the repository) + +# label 'ci' all automation-related steps and files +#'CI': +# - changed-files: +# - any-glob-to-any-file: +# - '.editorconfig' +# - '.flake8' +# - '.pre-commit-config.yaml' +{%- if cookiecutter.make_docs != 'y' %} +# - '.readthedocs.yml' +{%- endif %} +# - '.yamllint.yml' +# - '.github/workflows/*' +{%- if cookiecutter.make_docs != 'y' %} +# - 'docs/Makefile' +{%- endif %} +# - 'tox.ini' +# - 'Makefile' + +{%- if cookiecutter.make_docs != 'y' %} +# label 'docs' all documentation-related steps and files +#'docs': +# - changed-files: +# - any-glob-to-any-file: +# - '.readthedocs.yml' +{%- if cookiecutter.open_source_license != 'Not open source' %} +# - '.zenodo.json' +{%- endif %} +# - 'docs/**/*' +# - 'environment-docs.yml' +{%- if cookiecutter.create_author_file != 'y' %} +# - 'AUTHORS.rst' +{%- endif %} +# - 'CONTRIBUTING.rst' +# - 'CODE_OF_CONDUCT.md' +# - 'DISCUSSION_TEMPLATE/**/*' +# - 'ISSUE_TEMPLATE/**/*' +# - 'ISSUE_TEMPLATE.md' +# - 'PULL_REQUEST_TEMPLATE.md' +# - 'README.rst' +{%- endif %} diff --git a/{{cookiecutter.project_slug}}/.github/workflows/actions-versions-updater.yml b/{{cookiecutter.project_slug}}/.github/workflows/actions-versions-updater.yml deleted file mode 100644 index 44f316732..000000000 --- a/{{cookiecutter.project_slug}}/.github/workflows/actions-versions-updater.yml +++ /dev/null @@ -1,28 +0,0 @@ -# This workflow requires a personal access token repository secret named `BUMPVERSION_TOKEN` with the following privileges: -# - repo -# - workflow - -name: GitHub Actions Version Updater - -on: - schedule: - # 12:00 AM on the first of every month - - cron: '0 0 1 * *' - workflow_dispatch: - -jobs: - build: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4.1.1 - with: - # This requires a personal access token with the privileges to push directly to `main` - token: __BUMPVERSION_TOKEN__ - persist-credentials: true - - name: Run GitHub Actions Version Updater - uses: saadmk11/github-actions-version-updater@v0.8.1 - with: - token: __BUMPVERSION_TOKEN__ - committer_email: 'bumpversion[bot]@ouranos.ca' - committer_username: 'update-github-actions[bot]' - pull_request_title: '[bot] Update GitHub Action Versions' diff --git a/{{cookiecutter.project_slug}}/.github/workflows/bump-version.yml b/{{cookiecutter.project_slug}}/.github/workflows/bump-version.yml index c6e52a699..5b5e108c2 100644 --- a/{{cookiecutter.project_slug}}/.github/workflows/bump-version.yml +++ b/{{cookiecutter.project_slug}}/.github/workflows/bump-version.yml @@ -1,6 +1,7 @@ -# This workflow requires a personal access token named `BUMPVERSION_TOKEN` with the following privileges: -# - repo -# - workflow +# This workflow requires a personal access token named `BUMP_VERSION_TOKEN` with the following privileges: +# - Contents: Read and Write +# - Metadata: Read-Only +# - Pull Requests: Read and Write name: "Bump Patch Version" @@ -39,10 +40,25 @@ on: - {{ cookiecutter.project_slug }}/__init__.py workflow_dispatch: +permissions: + contents: read + jobs: bump_patch_version: runs-on: ubuntu-latest + permissions: + actions: read + contents: write steps: + - name: Harden Runner + uses: step-security/harden-runner@eb238b55efaa70779f274895e782ed17c84f2895 # v2.6.1 + with: + disable-sudo: true + egress-policy: block + allowed-endpoints: > + files.pythonhosted.org:443 + github.com:443 + pypi.org:443 - uses: actions/checkout@v4 with: persist-credentials: false @@ -65,5 +81,5 @@ jobs: uses: ad-m/github-push-action@master with: force: false - github_token: __BUMPVERSION_TOKEN__ + github_token: __BUMP_VERSION_TOKEN__ branch: __GITHUB_REF__ diff --git a/{{cookiecutter.project_slug}}/.github/workflows/cache-cleaner.yml b/{{cookiecutter.project_slug}}/.github/workflows/cache-cleaner.yml new file mode 100644 index 000000000..675363860 --- /dev/null +++ b/{{cookiecutter.project_slug}}/.github/workflows/cache-cleaner.yml @@ -0,0 +1,49 @@ +# Example taken from https://docs.github.com/en/actions/using-workflows/caching-dependencies-to-speed-up-workflows#managing-caches +name: Cleanup Caches on Pull Request Merge +on: + pull_request: + types: + - closed + +permissions: # added using https://github.com/step-security/secure-repo + contents: read + +jobs: + cleanup: + name: Cleanup + runs-on: ubuntu-latest + steps: + - name: Harden Runner + uses: step-security/harden-runner@eb238b55efaa70779f274895e782ed17c84f2895 # v2.6.1 + with: + disable-sudo: true + egress-policy: block + allowed-endpoints: > + api.github.com:443 + github.com:443 + objects.githubusercontent.com:443 + + - uses: actions/checkout@v4.1.1 + + - name: Cleanup + run: | + {%- raw %} + gh extension install actions/gh-actions-cache + + REPO=${{ github.repository }} + BRANCH="refs/pull/${{ github.event.pull_request.number }}/merge" + + echo "Fetching list of cache key" + cacheKeysForPR=$(gh actions-cache list -R $REPO -B $BRANCH -L 100 | cut -f 1 ) + + ## Setting this to not fail the workflow while deleting cache keys. + set +e + echo "Deleting caches..." + for cacheKey in $cacheKeysForPR + do + gh actions-cache delete $cacheKey -R $REPO -B $BRANCH --confirm + done + echo "Done" + {%- endraw %} + env: + GH_TOKEN: __GITHUB_TOKEN__ diff --git a/{{cookiecutter.project_slug}}/.github/workflows/dependency-review.yml b/{{cookiecutter.project_slug}}/.github/workflows/dependency-review.yml new file mode 100644 index 000000000..bfe444fd3 --- /dev/null +++ b/{{cookiecutter.project_slug}}/.github/workflows/dependency-review.yml @@ -0,0 +1,31 @@ +# Dependency Review Action +# +# This Action will scan dependency manifest files that change as part of a Pull Request, surfacing known-vulnerable versions of the packages declared or updated in the PR. Once installed, if the workflow run is marked as required, PRs introducing known-vulnerable packages will be blocked from merging. +# +# Source repository: https://github.com/actions/dependency-review-action +# Public documentation: https://docs.github.com/en/code-security/supply-chain-security/understanding-your-software-supply-chain/about-dependency-review#dependency-review-enforcement +name: 'Dependency Review' +on: + pull_request: + +permissions: + contents: read + +jobs: + dependency-review: + runs-on: ubuntu-latest + steps: + - name: Harden Runner + uses: step-security/harden-runner@eb238b55efaa70779f274895e782ed17c84f2895 # v2.6.1 + with: + disable-sudo: true + egress-policy: block + allowed-endpoints: > + api.github.com:443 + github.com:443 + + - name: 'Checkout Repository' + uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 + + - name: 'Dependency Review' + uses: actions/dependency-review-action@4901385134134e04cec5fbe5ddfe3b2c5bd5d97 diff --git a/{{cookiecutter.project_slug}}/.github/workflows/first_pull_request.yml b/{{cookiecutter.project_slug}}/.github/workflows/first-pull-request.yml similarity index 86% rename from {{cookiecutter.project_slug}}/.github/workflows/first_pull_request.yml rename to {{cookiecutter.project_slug}}/.github/workflows/first-pull-request.yml index 16c472f92..dfaca22d2 100644 --- a/{{cookiecutter.project_slug}}/.github/workflows/first_pull_request.yml +++ b/{{cookiecutter.project_slug}}/.github/workflows/first-pull-request.yml @@ -10,6 +10,14 @@ jobs: name: Welcome runs-on: ubuntu-latest steps: + - name: Harden Runner + uses: step-security/harden-runner@eb238b55efaa70779f274895e782ed17c84f2895 # v2.6.1 + with: + disable-sudo: true + egress-policy: block + allowed-endpoints: > + api.github.com:443 + - uses: actions/github-script@v6 with: script: | diff --git a/{{cookiecutter.project_slug}}/.github/workflows/label.yml b/{{cookiecutter.project_slug}}/.github/workflows/label.yml new file mode 100644 index 000000000..291be22c2 --- /dev/null +++ b/{{cookiecutter.project_slug}}/.github/workflows/label.yml @@ -0,0 +1,37 @@ +# This workflow will triage pull requests and apply a label based on the +# paths that are modified in the pull request. +# +# To use this workflow, you will need to set up a .github/labeler.yml +# file with configuration. For more information, see: +# https://github.com/actions/labeler/blob/master/README.md + +name: Labeler +on: + pull_request_target: + # Note: potential security risk from this action using pull_request_target. + # Do not add actions in here which need a checkout of the repo, and do not use any caching in here. + # See: https://docs.github.com/en/actions/using-workflows/events-that-trigger-workflows#pull_request_target + +permissions: + contents: read + +jobs: + label: + name: Label + runs-on: ubuntu-latest + permissions: + checks: write + contents: read + pull-requests: write + steps: + - name: Harden Runner + uses: step-security/harden-runner@eb238b55efaa70779f274895e782ed17c84f2895 # v2.6.1 + with: + disable-sudo: true + egress-policy: block + allowed-endpoints: > + api.github.com:443 + + - uses: actions/labeler@v5.0.0 + with: + repo-token: __GITHUB_TOKEN__ diff --git a/{{cookiecutter.project_slug}}/.github/workflows/main.yml b/{{cookiecutter.project_slug}}/.github/workflows/main.yml index 51411c9d1..3350e0069 100644 --- a/{{cookiecutter.project_slug}}/.github/workflows/main.yml +++ b/{{cookiecutter.project_slug}}/.github/workflows/main.yml @@ -13,6 +13,16 @@ on: - {{ cookiecutter.project_slug }}/__init__.py pull_request: +concurrency: +{%- raw %} + # For a given workflow, if we push to the same branch, cancel all previous builds on that branch except on master. + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: ${{ github.ref != 'refs/heads/main' }} +{%- endraw %} + +permissions: + contents: read + jobs: lint: name: Lint (Python__PYTHON_VERSION__) @@ -22,10 +32,6 @@ jobs: python-version: - "3.x" steps: - - name: Cancel previous runs - uses: styfle/cancel-workflow-action@0.11.0 - with: - access_token: __GITHUB_TOKEN__ - uses: actions/checkout@v4 - name: Set up Python__PYTHON_VERSION__ uses: actions/setup-python@v4 @@ -53,6 +59,8 @@ jobs: python-version: "3.10" - tox-env: "py311" python-version: "3.11" + - tox-env: "py312" + python-version: "3.12" steps: - uses: actions/checkout@v4 - name: Set up Python__PYTHON_VERSION__ @@ -83,6 +91,10 @@ jobs: run: shell: bash -l {0} steps: + - name: Harden Runner + uses: step-security/harden-runner@eb238b55efaa70779f274895e782ed17c84f2895 # v2.6.1 + with: + egress-policy: audit - uses: actions/checkout@v4 - name: Setup Conda (Micromamba) with Python__PYTHON_VERSION__ uses: mamba-org/setup-micromamba@v1 diff --git a/{{cookiecutter.project_slug}}/.github/workflows/publish-pypi.yml b/{{cookiecutter.project_slug}}/.github/workflows/publish-pypi.yml index fd8758a5d..c0bb56d40 100644 --- a/{{cookiecutter.project_slug}}/.github/workflows/publish-pypi.yml +++ b/{{cookiecutter.project_slug}}/.github/workflows/publish-pypi.yml @@ -5,6 +5,9 @@ on: types: - published +permissions: + contents: read + jobs: build-n-publish-pypi: name: Build and publish Python 🐍 distributions 📦 to PyPI @@ -14,6 +17,10 @@ jobs: # IMPORTANT: this permission is mandatory for trusted publishing id-token: write steps: + - name: Harden Runner + uses: step-security/harden-runner@eb238b55efaa70779f274895e782ed17c84f2895 # v2.6.1 + with: + egress-policy: audit - uses: actions/checkout@v4 - name: Set up Python3 uses: actions/setup-python@v4 diff --git a/{{cookiecutter.project_slug}}/.github/workflows/scorecard.yml b/{{cookiecutter.project_slug}}/.github/workflows/scorecard.yml new file mode 100644 index 000000000..7fe6bbbbe --- /dev/null +++ b/{{cookiecutter.project_slug}}/.github/workflows/scorecard.yml @@ -0,0 +1,82 @@ +# This workflow uses actions that are not certified by GitHub. +# They are provided by a third-party and are governed by separate terms of service, privacy +# policy, and support documentation. + +name: Scorecard supply-chain security +on: + # For Branch-Protection check. Only the default branch is supported. See + # https://github.com/ossf/scorecard/blob/main/docs/checks.md#branch-protection + branch_protection_rule: + # To guarantee Maintained check is occasionally updated. See + # https://github.com/ossf/scorecard/blob/main/docs/checks.md#maintained + schedule: + - cron: '41 8 * * 4' + push: + branches: + - master + +# Declare default permissions as read only. +permissions: read-all + +jobs: + analysis: + name: Scorecard analysis + runs-on: ubuntu-latest + permissions: + # Needed to upload the results to code-scanning dashboard. + security-events: write + # Needed to publish results and get a badge (see publish_results below). + id-token: write + steps: + - name: Harden Runner + uses: step-security/harden-runner@eb238b55efaa70779f274895e782ed17c84f2895 # v2.6.1 + with: + disable-sudo: true + egress-policy: block + allowed-endpoints: > + api.github.com:443 + api.osv.dev:443 + api.securityscorecards.dev:443 + fulcio.sigstore.dev:443 + github.com:443 + oss-fuzz-build-logs.storage.googleapis.com:443 + rekor.sigstore.dev:443 + tuf-repo-cdn.sigstore.dev:443 + www.bestpractices.dev:443 + + - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 + with: + persist-credentials: false + + - name: Run analysis + uses: ossf/scorecard-action@0864cf19026789058feabb7e87baa5f140aac736 + with: + results_file: results.sarif + results_format: sarif + # This job step requires a personal access token named `OPENSSF_SCORECARD_TOKEN` with the following privileges: + # - Administration: Read-Only + # - Metadata: Read-Only + # - Webhooks: Read-Only + # To create the PAT, follow the steps in https://github.com/ossf/scorecard-action#authentication-with-pat. + repo_token: __OPENSSF_SCORECARD_TOKEN__ + + # Public repositories: + # - Publish results to OpenSSF REST API for easy access by consumers + # - Allows the repository to include the Scorecard badge. + # - See https://github.com/ossf/scorecard-action#publishing-results. + publish_results: true + + # Upload the results as artifacts (optional). Commenting out will disable uploads of run results in SARIF + # format to the repository Actions tab. + - name: Upload artifact + uses: actions/upload-artifact@694cdabd8bdb0f10b2cea11669e1bf5453eed0a6 + with: + name: SARIF file + path: results.sarif + retention-days: 5 + + # Upload the results to GitHub's code scanning dashboard. + - name: Upload to code-scanning + uses: github/codeql-action/upload-sarif@e5f05b81d5b6ff8cfa111c80c22c5fd02a384118 # 3.23.0 + with: + sarif_file: results.sarif diff --git a/{{cookiecutter.project_slug}}/.github/workflows/tag-testpypi.yml b/{{cookiecutter.project_slug}}/.github/workflows/tag-testpypi.yml index e78c80c64..81bb4dc91 100644 --- a/{{cookiecutter.project_slug}}/.github/workflows/tag-testpypi.yml +++ b/{{cookiecutter.project_slug}}/.github/workflows/tag-testpypi.yml @@ -5,13 +5,21 @@ on: tags: - 'v*.*' # Push events to matching v*, i.e. v1.0, v20.15.10 -jobs: +permissions: + contents: read +jobs: release: name: Create Release from tag runs-on: ubuntu-latest if: startsWith(github.ref, 'refs/tags/v') && endsWith(github.ref, '.0') + permissions: + contents: write steps: + - name: Harden Runner + uses: step-security/harden-runner@eb238b55efaa70779f274895e782ed17c84f2895 # v2.6.1 + with: + egress-policy: audit - name: Checkout code uses: actions/checkout@v4 - name: Create Release @@ -33,6 +41,10 @@ jobs: # IMPORTANT: this permission is mandatory for trusted publishing id-token: write steps: + - name: Harden Runner + uses: step-security/harden-runner@eb238b55efaa70779f274895e782ed17c84f2895 # v2.6.1 + with: + egress-policy: audit - uses: actions/checkout@v4 - name: Set up Python3 uses: actions/setup-python@v4 diff --git a/{{cookiecutter.project_slug}}/.github/workflows/workflow-warning.yml b/{{cookiecutter.project_slug}}/.github/workflows/workflow-warning.yml new file mode 100644 index 000000000..b5afa312a --- /dev/null +++ b/{{cookiecutter.project_slug}}/.github/workflows/workflow-warning.yml @@ -0,0 +1,71 @@ +name: Workflow Changes Warnings + +on: + # Note: potential security risk from this action using pull_request_target. + # Do not add actions in here which need a checkout of the repo, and do not use any caching in here. + # See: https://docs.github.com/en/actions/using-workflows/events-that-trigger-workflows#pull_request_target + pull_request_target: + types: + - opened + - reopened + - synchronize + paths: + - .github/workflows/*.yml + +permissions: + contents: read + +{% raw -%} +jobs: + comment-concerning-workflow-changes: + name: Comment Concerning Workflow Changes + runs-on: ubuntu-latest + if: | + (github.event.pull_request.head.repo.full_name != github.event.pull_request.base.repo.full_name) + permissions: + contents: read + pull-requests: write + steps: + - name: Harden Runner + uses: step-security/harden-runner@eb238b55efaa70779f274895e782ed17c84f2895 # v2.6.1 + with: + disable-sudo: true + egress-policy: block + allowed-endpoints: > + api.github.com:443 + - name: Find comment + uses: peter-evans/find-comment@a54c31d7fa095754bfef525c0c8e5e5674c4b4b1 # v2.4.0 + id: fc + with: + issue-number: ${{ github.event.pull_request.number }} + comment-author: 'github-actions[bot]' + body-includes: | + This Pull Request modifies GitHub workflows and is coming from a fork. + - name: Create comment + if: | + (steps.fc.outputs.comment-id == '') && + (!contains(github.event.pull_request.labels.*.name, 'approved')) && + (github.event.pull_request.head.repo.full_name != github.event.pull_request.base.repo.full_name) + uses: peter-evans/create-or-update-comment@23ff15729ef2fc348714a3bb66d2f655ca9066f2 # v3.1.0 + with: + comment-id: ${{ steps.fc.outputs.comment-id }} + issue-number: ${{ github.event.pull_request.number }} + body: | + > **Warning** + > This Pull Request modifies GitHub Workflows and is coming from a fork. + **It is very important for the reviewer to ensure that the workflow changes are appropriate.** + edit-mode: replace + - name: Update comment + if: | + contains(github.event.pull_request.labels.*.name, 'approved') + uses: peter-evans/create-or-update-comment@23ff15729ef2fc348714a3bb66d2f655ca9066f2 # v3.1.0 + with: + comment-id: ${{ steps.fc.outputs.comment-id }} + issue-number: ${{ github.event.pull_request.number }} + body: | + > **Note** + > Changes have been approved by a maintainer. + reactions: | + hooray + edit-mode: append +{%- endraw %} diff --git a/{{cookiecutter.project_slug}}/.pre-commit-config.yaml b/{{cookiecutter.project_slug}}/.pre-commit-config.yaml index c56e93190..3ca404b08 100644 --- a/{{cookiecutter.project_slug}}/.pre-commit-config.yaml +++ b/{{cookiecutter.project_slug}}/.pre-commit-config.yaml @@ -3,12 +3,12 @@ default_language_version: repos: - repo: https://github.com/asottile/pyupgrade - rev: v3.4.0 + rev: v3.15.0 hooks: - id: pyupgrade args: [--py38-plus] - repo: https://github.com/pre-commit/pre-commit-hooks - rev: v4.4.0 + rev: v4.5.0 hooks: - id: trailing-whitespace - id: end-of-file-fixer @@ -30,41 +30,41 @@ repos: hooks: - id: rst-inline-touching-normal {%- if cookiecutter.use_black == 'y' %} - - repo: https://github.com/psf/black - rev: 23.11.0 + - repo: https://github.com/psf/black-pre-commit-mirror + rev: 24.1.1 hooks: - id: black exclude: ^docs/ - repo: https://github.com/PyCQA/isort - rev: 5.12.0 + rev: 5.13.2 hooks: - id: isort exclude: ^docs/ {%- endif %} - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.1.4 + rev: v0.1.14 hooks: - id: ruff - repo: https://github.com/pycqa/flake8 - rev: 6.0.0 + rev: 7.0.0 hooks: - id: flake8 additional_dependencies: [ 'flake8-alphabetize', 'flake8-rst-docstrings' ] args: [ '--config=.flake8' ] {%- if cookiecutter.use_black == 'y' %} - repo: https://github.com/keewis/blackdoc - rev: v0.3.8 + rev: v0.3.9 hooks: - id: blackdoc - additional_dependencies: [ 'black==23.11.0' ] + additional_dependencies: [ 'black==24.1.1' ] {%- endif %} - repo: https://github.com/adrienverge/yamllint.git - rev: v1.32.0 + rev: v1.33.0 hooks: - id: yamllint args: ['--config-file', '.yamllint.yaml'] - repo: https://github.com/python-jsonschema/check-jsonschema - rev: 0.27.1 + rev: 0.27.3 hooks: - id: check-github-workflows - id: check-readthedocs diff --git a/{{cookiecutter.project_slug}}/README.rst b/{{cookiecutter.project_slug}}/README.rst index 88013bd4a..b55872822 100644 --- a/{{cookiecutter.project_slug}}/README.rst +++ b/{{cookiecutter.project_slug}}/README.rst @@ -5,21 +5,19 @@ {%- if is_open_source %} -.. image:: https://img.shields.io/pypi/v/{{ cookiecutter.project_slug }}.svg - :target: https://pypi.python.org/pypi/{{ cookiecutter.project_slug }} - :alt: PyPI - -.. image:: https://github.com/{{ cookiecutter.github_username }}/{{ cookiecutter.project_slug }}/actions/workflows/main.yml/badge.svg - :target: https://github.com/{{ cookiecutter.github_username }}/{{ cookiecutter.project_slug }}/actions - :alt: Build Status - -.. image:: https://readthedocs.org/projects/{{ cookiecutter.project_slug | replace("_", "-") }}/badge/?version=latest - :target: https://{{ cookiecutter.project_slug | replace("_", "-") }}.readthedocs.io/en/latest/?version=latest - :alt: Documentation Status - -.. image:: https://img.shields.io/github/license/{{ cookiecutter.github_username }}/{{ cookiecutter.project_slug }}.svg - :target: https://github.com/{{ cookiecutter.github_username }}/{{ cookiecutter.project_slug }}/blob/main/LICENSE - :alt: License ++----------------------------+-----------------------------------------------------+ +| Versions | |pypi| |versions| | ++----------------------------+-----------------------------------------------------+ +| Documentation and Support | |docs| | ++----------------------------+-----------------------------------------------------+ +{%- if is_open_source %} +| Open Source | |license| |ossf| | +{%- endif %} ++----------------------------+-----------------------------------------------------+ +| Coding Standards | |black| |ruff| |pre-commit| | ++----------------------------+-----------------------------------------------------+ +| Development Status | |status| |build| |coveralls| | ++----------------------------+-----------------------------------------------------+ {%- endif %} {%- if cookiecutter.add_pyup_badge == 'y' %} @@ -48,3 +46,52 @@ This package was created with Cookiecutter_ and the `Ouranosinc/cookiecutter-pyp .. _Cookiecutter: https://github.com/cookiecutter/cookiecutter .. _`Ouranosinc/cookiecutter-pypackage`: https://github.com/Ouranosinc/cookiecutter-pypackage + + +.. |black| image:: https://img.shields.io/badge/code%20style-black-000000.svg + :target: https://github.com/psf/black + :alt: Python Black + +.. |build| image:: https://github.com/{{ cookiecutter.github_username }}/{{ cookiecutter.project_slug }}/actions/workflows/main.yml/badge.svg + :target: https://github.com/{{ cookiecutter.github_username }}/{{ cookiecutter.project_slug }}/actions + :alt: Build Status + +.. |coveralls| image:: https://coveralls.io/repos/github/{{ cookiecutter.github_username }}/{{ cookiecutter.project_slug }}/badge.svg + :target: https://coveralls.io/github/{{ cookiecutter.github_username }}/{{ cookiecutter.project_slug }} + :alt: Coveralls + +.. |docs| image:: https://readthedocs.org/projects/{{ cookiecutter.project_slug | replace("_", "-") }}/badge/?version=latest + :target: https://{{ cookiecutter.project_slug | replace("_", "-") }}.readthedocs.io/en/latest/?version=latest + :alt: Documentation Status + +{%- if is_open_source %} + +.. |license| image:: https://img.shields.io/github/license/{{ cookiecutter.github_username }}/{{ cookiecutter.project_slug }}.svg + :target: https://github.com/{{ cookiecutter.github_username }}/{{ cookiecutter.project_slug }}/blob/main/LICENSE + :alt: License + +.. |ossf| image:: https://api.securityscorecards.dev/projects/github.com/{{ cookiecutter.github_username }}/{{ cookiecutter.project_slug }}/badge + :target: https://securityscorecards.dev/viewer/?uri=github.com/{{ cookiecutter.github_username }}/{{ cookiecutter.project_slug }} + :alt: OpenSSF Scorecard + +{%- endif %} + +.. |pre-commit| image:: https://results.pre-commit.ci/badge/github/{{ cookiecutter.github_username }}/{{ cookiecutter.project_slug }}/main.svg + :target: https://results.pre-commit.ci/latest/github/{{ cookiecutter.github_username }}/{{ cookiecutter.project_slug }}/main + :alt: pre-commit.ci status + +.. |pypi| image:: https://img.shields.io/pypi/v/{{ cookiecutter.project_slug }}.svg + :target: https://pypi.python.org/pypi/{{ cookiecutter.project_slug }} + :alt: PyPI + +.. |ruff| image:: https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/ruff/main/assets/badge/v2.json + :target: https://github.com/astral-sh/ruff + :alt: Ruff + +.. |status| image:: https://www.repostatus.org/badges/latest/active.svg + :target: https://www.repostatus.org/#active + :alt: Project Status: Active – The project has reached a stable, usable state and is being actively developed. + +.. |versions| image:: https://img.shields.io/pypi/pyversions/{{ cookiecutter.project_slug }}.svg + :target: https://pypi.python.org/pypi/{{ cookiecutter.project_slug }} + :alt: Supported Python Versions diff --git a/{{cookiecutter.project_slug}}/environment-dev.yml b/{{cookiecutter.project_slug}}/environment-dev.yml index 5ae734a4c..b8cf59cb2 100644 --- a/{{cookiecutter.project_slug}}/environment-dev.yml +++ b/{{cookiecutter.project_slug}}/environment-dev.yml @@ -5,7 +5,7 @@ dependencies: - python >=3.9,<3.12 # Dev tools and testing - pip >=23.1.2 - - bump-my-version >=0.12.0 + - bump-my-version >=0.17.1 - watchdog >=3.0.0 - flake8 >=6.1.0 - flake8-rst-docstrings >=0.3.0 @@ -21,8 +21,8 @@ dependencies: - pytest-cov>=4.0.0 {%- endif %} {%- if cookiecutter.use_black == 'y' %} - - black >=23.11.0 - - blackdoc >=0.3.9 - - isort >=5.12.0 + - black ==24.11.1 + - blackdoc ==0.3.9 + - isort ==5.13.2 {%- endif %} - pre-commit >=3.3.2 diff --git a/{{cookiecutter.project_slug}}/pyproject.toml b/{{cookiecutter.project_slug}}/pyproject.toml index 996914a8d..96df918d0 100644 --- a/{{cookiecutter.project_slug}}/pyproject.toml +++ b/{{cookiecutter.project_slug}}/pyproject.toml @@ -35,6 +35,7 @@ classifiers = [ "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", "Programming Language :: Python :: Implementation :: CPython" ] dynamic = ["description", "version"] @@ -46,12 +47,12 @@ dependencies = [ dev = [ # Dev tools and testing "pip>=23.1.2", - "bump-my-version>=0.12.0", + "bump-my-version>=0.17.1", "watchdog>=3.0.0", "flake8>=6.1.0", "flake8-alphabetize>=0.0.21", "flake8-rst-docstrings>=0.3.0", - "flit", + "flit>=3.9.0", "tox>=4.5.1", "coverage>=6.2.2,<7.0.0", "coveralls>=3.3.1", @@ -63,9 +64,9 @@ dev = [ "pytest-cov>=4.0.0", {%- endif %} {%- if cookiecutter.use_black == 'y' %} - "black>=23.10.1", - "blackdoc>=0.3.9", - "isort>=5.12.0", + "black==24.11.1", + "blackdoc==0.3.9", + "isort==5.13.2", {%- endif %} "pre-commit>=3.3.2" ] @@ -110,18 +111,23 @@ target-version = [ "py38", "py39", "py310", - "py311" + "py311", + "py312" ] {%- endif %} [tool.bumpversion] current_version = "{{ cookiecutter.version }}" commit = true +commit_args = "--no-verify" tag = false tag_name = "v{new_version}" allow_dirty = false -serialize = ["{major}.{minor}.{patch}"] -parse = "(?P\\d+)\\.(?P\\d+)\\.(?P\\d+)" +parse = "(?P\\d+)\\.(?P\\d+)\\.(?P\\d+)(\\-(?P[a-z]+)(\\.(?P\\d+)))?" +serialize = [ + "{major}.{minor}.{patch}-{release}.{build}", + "{major}.{minor}.{patch}" +] [[tool.bumpversion.files]] filename = "{{ cookiecutter.project_slug }}/__init__.py" @@ -140,6 +146,16 @@ search = "\"version\": \"{current_version}\"" replace = "\"version\": \"{new_version}\"" {%- endif %} +[tool.bumpversion.parts.build] +independent = false + +[tool.bumpversion.parts.release] +optional_value = "release" +values = [ + "dev", + "release" +] + [tool.coverage.run] relative_files = true include = ["{{ cookiecutter.project_slug }}/*"] diff --git a/{{cookiecutter.project_slug}}/tox.ini b/{{cookiecutter.project_slug}}/tox.ini index e64e405f4..8553467dc 100644 --- a/{{cookiecutter.project_slug}}/tox.ini +++ b/{{cookiecutter.project_slug}}/tox.ini @@ -2,7 +2,7 @@ min_version = 4.0 envlist = lint - py{38,39,310,311} + py{38,39,310,311,312} {%- if cookiecutter.make_docs == 'y' %} docs {%- endif %} @@ -17,9 +17,9 @@ opts = skip_install = True deps = {%- if cookiecutter.use_black == 'y' %} - black - blackdoc - isort + black ==24.1.1 + blackdoc ==0.3.9 + isort ==5.13.2 {%- endif %} flake8 ruff diff --git a/{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/cli.py b/{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/cli.py index c1c953f1b..0e58b2ba9 100644 --- a/{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/cli.py +++ b/{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/cli.py @@ -1,8 +1,8 @@ """Console script for {{cookiecutter.project_slug}}.""" -{%- if cookiecutter.command_line_interface|lower == 'argparse' %} +{% if cookiecutter.command_line_interface|lower == 'argparse' -%} import argparse -{%- endif %} +{%- endif -%} import sys {%- if cookiecutter.command_line_interface|lower == 'click' %}