diff --git a/.github/workflows/auto-dependabot.yaml b/.github/workflows/auto-dependabot.yaml index 58c5355..9b1a5ef 100644 --- a/.github/workflows/auto-dependabot.yaml +++ b/.github/workflows/auto-dependabot.yaml @@ -28,7 +28,7 @@ jobs: steps: - name: Generate GitHub App token id: app-token - uses: actions/create-github-app-token@f8d387b68d61c58ab83c6c016672934102569859 # v3.0.0 + uses: actions/create-github-app-token@29824e69f54612133e76f7eaac726eef6c875baf # v2.2.1 with: app-id: ${{ secrets.FREQUENZ_AUTO_DEPENDABOT_APP_ID }} private-key: ${{ secrets.FREQUENZ_AUTO_DEPENDABOT_APP_PRIVATE_KEY }} diff --git a/.github/workflows/black-migration.yaml b/.github/workflows/black-migration.yaml index d22b182..1969c46 100644 --- a/.github/workflows/black-migration.yaml +++ b/.github/workflows/black-migration.yaml @@ -66,7 +66,7 @@ jobs: # Read/update pull request metadata and labels. permission-pull-requests: write - name: Migrate - uses: llucax/gh-action-dependabot-migrate@90f41ef501378754ffbcd3a75bc907ac9fe1b31e # internal-script + uses: frequenz-floss/gh-action-dependabot-migrate@b389f72f9282346920150a67495efbae450ac07b # v1.1.0 with: migration-script: | import os diff --git a/.github/workflows/ci-pr.yaml b/.github/workflows/ci-pr.yaml index ac6905d..015d585 100644 --- a/.github/workflows/ci-pr.yaml +++ b/.github/workflows/ci-pr.yaml @@ -3,6 +3,10 @@ name: Test PR on: pull_request: +permissions: + # Read repository contents for checkout and dependency resolution only. + contents: read + env: # Please make sure this version is included in the `matrix`, as the # `matrix` section can't use `env`, so it must be entered manually @@ -17,7 +21,7 @@ jobs: steps: - name: Run nox - uses: frequenz-floss/gh-action-nox@v1.1.1 + uses: frequenz-floss/gh-action-nox@e1351cf45e05e85afc1c79ab883e06322892d34c # v1.1.0 with: python-version: "3.11" nox-session: ci_checks_max @@ -27,15 +31,15 @@ jobs: runs-on: ubuntu-24.04 steps: - name: Setup Git - uses: frequenz-floss/gh-action-setup-git@v1.0.0 + uses: frequenz-floss/gh-action-setup-git@16952aac3ccc01d27412fe0dea3ea946530dcace # v1.0.0 - name: Fetch sources - uses: actions/checkout@v6 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: submodules: true - name: Setup Python - uses: frequenz-floss/gh-action-setup-python-with-deps@v1.0.4 + uses: frequenz-floss/gh-action-setup-python-with-deps@0d0d77eac3b54799f31f25a1060ef2c6ebdf9299 # v1.0.2 with: python-version: ${{ env.DEFAULT_PYTHON_VERSION }} dependencies: .[dev-mkdocs] @@ -44,11 +48,14 @@ jobs: env: MIKE_VERSION: gh-${{ github.job }} run: | - mike deploy $MIKE_VERSION - mike set-default $MIKE_VERSION + # mike is installed as a console script, not a runnable module. + # Run the installed script under isolated mode to avoid importing from + # the workspace when building docs from checked-out code. + python -I "$(command -v mike)" deploy "$MIKE_VERSION" + python -I "$(command -v mike)" set-default "$MIKE_VERSION" - name: Upload site - uses: actions/upload-artifact@v7 + uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0 with: name: docs-site path: site/ diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 95f10f8..c3b4cfb 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -15,6 +15,10 @@ on: - 'dependabot/**' workflow_dispatch: +permissions: + # Read repository contents for checkout and dependency resolution only. + contents: read + env: # Please make sure this version is included in the `matrix`, as the # `matrix` section can't use `env`, so it must be entered manually @@ -28,11 +32,9 @@ jobs: strategy: fail-fast: false matrix: - arch: - - amd64 - - arm - os: + platform: - ubuntu-24.04 + - ubuntu-24.04-arm python: - "3.11" - "3.12" @@ -41,11 +43,11 @@ jobs: # that uses the same venv to run multiple linting sessions - "ci_checks_max" - "pytest_min" - runs-on: ${{ matrix.os }}${{ matrix.arch != 'amd64' && format('-{0}', matrix.arch) || '' }} + runs-on: ${{ matrix.platform }} steps: - name: Run nox - uses: frequenz-floss/gh-action-nox@v1.1.1 + uses: frequenz-floss/gh-action-nox@e1351cf45e05e85afc1c79ab883e06322892d34c # v1.1.0 with: python-version: ${{ matrix.python }} nox-session: ${{ matrix.nox-session }} @@ -60,7 +62,9 @@ jobs: needs: ["nox"] # We skip this job only if nox was also skipped if: always() && needs.nox.result != 'skipped' - runs-on: ubuntu-24.04 + runs-on: ubuntu-slim + # Drop token permissions: this job only checks matrix status from `needs`. + permissions: {} env: DEPS_RESULT: ${{ needs.nox.result }} steps: @@ -76,24 +80,24 @@ jobs: steps: - name: Setup Git - uses: frequenz-floss/gh-action-setup-git@v1.0.0 + uses: frequenz-floss/gh-action-setup-git@16952aac3ccc01d27412fe0dea3ea946530dcace # v1.0.0 - name: Fetch sources - uses: actions/checkout@v6 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: submodules: true - name: Setup Python - uses: frequenz-floss/gh-action-setup-python-with-deps@v1.0.4 + uses: frequenz-floss/gh-action-setup-python-with-deps@0d0d77eac3b54799f31f25a1060ef2c6ebdf9299 # v1.0.2 with: python-version: ${{ env.DEFAULT_PYTHON_VERSION }} dependencies: build - name: Build the source and binary distribution - run: python -m build + run: python -Im build - name: Upload distribution files - uses: actions/upload-artifact@v7 + uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0 with: name: dist-packages path: dist/ @@ -105,25 +109,23 @@ jobs: strategy: fail-fast: false matrix: - arch: - - amd64 - - arm - os: + platform: - ubuntu-24.04 + - ubuntu-24.04-arm python: - "3.11" - "3.12" - runs-on: ${{ matrix.os }}${{ matrix.arch != 'amd64' && format('-{0}', matrix.arch) || '' }} + runs-on: ${{ matrix.platform }} steps: - name: Setup Git - uses: frequenz-floss/gh-action-setup-git@v1.0.0 + uses: frequenz-floss/gh-action-setup-git@16952aac3ccc01d27412fe0dea3ea946530dcace # v1.0.0 - name: Print environment (debug) run: env - name: Download package - uses: actions/download-artifact@v8 + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 with: name: dist-packages path: dist @@ -143,13 +145,13 @@ jobs: > pyproject.toml - name: Setup Python - uses: frequenz-floss/gh-action-setup-python-with-deps@v1.0.4 + uses: frequenz-floss/gh-action-setup-python-with-deps@0d0d77eac3b54799f31f25a1060ef2c6ebdf9299 # v1.0.2 with: python-version: ${{ matrix.python }} dependencies: dist/*.whl - name: Print installed packages (debug) - run: python -m pip freeze + run: python -Im pip freeze # This job runs if all the `test-installation` matrix jobs ran and succeeded. # It is only used to have a single job that we can require in branch @@ -161,7 +163,9 @@ jobs: needs: ["test-installation"] # We skip this job only if test-installation was also skipped if: always() && needs.test-installation.result != 'skipped' - runs-on: ubuntu-24.04 + runs-on: ubuntu-slim + # Drop token permissions: this job only checks matrix status from `needs`. + permissions: {} env: DEPS_RESULT: ${{ needs.test-installation.result }} steps: @@ -174,15 +178,15 @@ jobs: runs-on: ubuntu-24.04 steps: - name: Setup Git - uses: frequenz-floss/gh-action-setup-git@v1.0.0 + uses: frequenz-floss/gh-action-setup-git@16952aac3ccc01d27412fe0dea3ea946530dcace # v1.0.0 - name: Fetch sources - uses: actions/checkout@v6 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: submodules: true - name: Setup Python - uses: frequenz-floss/gh-action-setup-python-with-deps@v1.0.4 + uses: frequenz-floss/gh-action-setup-python-with-deps@0d0d77eac3b54799f31f25a1060ef2c6ebdf9299 # v1.0.2 with: python-version: ${{ env.DEFAULT_PYTHON_VERSION }} dependencies: .[dev-mkdocs] @@ -191,11 +195,14 @@ jobs: env: MIKE_VERSION: gh-${{ github.job }} run: | - mike deploy $MIKE_VERSION - mike set-default $MIKE_VERSION + # mike is installed as a console script, not a runnable module. + # Run the installed script under isolated mode to avoid importing from + # the workspace when building docs from checked-out code. + python -I "$(command -v mike)" deploy "$MIKE_VERSION" + python -I "$(command -v mike)" set-default "$MIKE_VERSION" - name: Upload site - uses: actions/upload-artifact@v7 + uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0 with: name: docs-site path: site/ @@ -207,18 +214,19 @@ jobs: if: github.event_name == 'push' runs-on: ubuntu-24.04 permissions: + # Push generated documentation updates to the `gh-pages` branch. contents: write steps: - name: Setup Git - uses: frequenz-floss/gh-action-setup-git@v1.0.0 + uses: frequenz-floss/gh-action-setup-git@16952aac3ccc01d27412fe0dea3ea946530dcace # v1.0.0 - name: Fetch sources - uses: actions/checkout@v6 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: submodules: true - name: Setup Python - uses: frequenz-floss/gh-action-setup-python-with-deps@v1.0.4 + uses: frequenz-floss/gh-action-setup-python-with-deps@0d0d77eac3b54799f31f25a1060ef2c6ebdf9299 # v1.0.2 with: python-version: ${{ env.DEFAULT_PYTHON_VERSION }} dependencies: .[dev-mkdocs] @@ -231,7 +239,7 @@ jobs: GIT_REF: ${{ github.ref }} GIT_SHA: ${{ github.sha }} run: | - python -m frequenz.repo.config.cli.version.mike.info + python -Im frequenz.repo.config.cli.version.mike.info - name: Fetch the gh-pages branch if: steps.mike-version.outputs.version @@ -252,13 +260,23 @@ jobs: GIT_REF: ${{ github.ref }} GIT_SHA: ${{ github.sha }} run: | - mike deploy --update-aliases --title "$TITLE" "$VERSION" $ALIASES + # Collect aliases into an array to avoid accidental (or malicious) + # shell injection when passing them to mike. + aliases=() + if test -n "$ALIASES"; then + read -r -a aliases <<<"$ALIASES" + fi + # mike is installed as a console script, not a runnable module. + # Run the installed script under isolated mode to avoid importing from + # the workspace when building docs from checked-out code. + python -I "$(command -v mike)" \ + deploy --update-aliases --title "$TITLE" "$VERSION" "${aliases[@]}" - name: Sort site versions if: steps.mike-version.outputs.version run: | git checkout gh-pages - python -m frequenz.repo.config.cli.version.mike.sort versions.json + python -Im frequenz.repo.config.cli.version.mike.sort versions.json git commit -a -m "Sort versions.json" - name: Publish site @@ -272,14 +290,12 @@ jobs: # Create a release only on tags creation if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags/v') permissions: - # We need write permissions on contents to create GitHub releases and on - # discussions to create the release announcement in the discussion forums + # Create GitHub releases and upload distribution artifacts. contents: write - discussions: write - runs-on: ubuntu-24.04 + runs-on: ubuntu-slim steps: - name: Download distribution files - uses: actions/download-artifact@v8 + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 with: name: dist-packages path: dist @@ -301,14 +317,14 @@ jobs: - name: Create GitHub release run: | set -ux - extra_opts= - if echo "$REF_NAME" | grep -- -; then extra_opts=" --prerelease"; fi + extra_opts=() + if echo "$REF_NAME" | grep -- -; then extra_opts+=(--prerelease); fi gh release create \ -R "$REPOSITORY" \ --notes-file RELEASE_NOTES.md \ --generate-notes \ - $extra_opts \ - $REF_NAME \ + "${extra_opts[@]}" \ + "$REF_NAME" \ dist/* env: REF_NAME: ${{ github.ref_name }} @@ -325,10 +341,10 @@ jobs: id-token: write steps: - name: Download distribution files - uses: actions/download-artifact@v8 + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 with: name: dist-packages path: dist - name: Publish the Python distribution to PyPI - uses: pypa/gh-action-pypi-publish@release/v1 + uses: pypa/gh-action-pypi-publish@ed0c53931b1dc9bd32cbe73a98c7f6766f8a527e # v1.13.0 diff --git a/.github/workflows/dco-merge-queue.yml b/.github/workflows/dco-merge-queue.yml index fb1cd90..7a4260d 100644 --- a/.github/workflows/dco-merge-queue.yml +++ b/.github/workflows/dco-merge-queue.yml @@ -3,9 +3,12 @@ name: DCO on: merge_group: +# Drop all token permissions: this workflow only runs a local echo command. +permissions: {} + jobs: DCO: - runs-on: ubuntu-latest + runs-on: ubuntu-slim if: ${{ github.actor != 'dependabot[bot]' }} steps: - run: echo "This DCO job runs on merge_queue event and doesn't check PR contents" diff --git a/.github/workflows/labeler.yml b/.github/workflows/labeler.yml index 8d02c13..393ddfc 100644 --- a/.github/workflows/labeler.yml +++ b/.github/workflows/labeler.yml @@ -5,9 +5,11 @@ on: [pull_request_target] jobs: Label: permissions: + # Read the labeler configuration from the repository. contents: read + # Add labels to pull requests. pull-requests: write - runs-on: ubuntu-latest + runs-on: ubuntu-slim steps: - name: Labeler # XXX: !!! SECURITY WARNING !!! diff --git a/.github/workflows/release-notes-check.yml b/.github/workflows/release-notes-check.yml index 41212e8..74ca95f 100644 --- a/.github/workflows/release-notes-check.yml +++ b/.github/workflows/release-notes-check.yml @@ -16,7 +16,10 @@ on: jobs: check-release-notes: name: Check release notes are updated - runs-on: ubuntu-latest + runs-on: ubuntu-slim + permissions: + # Read pull request metadata to evaluate labels and changed files. + pull-requests: read steps: - name: Check for a release notes update if: github.event_name == 'pull_request' diff --git a/.github/workflows/repo-config-migration.yaml b/.github/workflows/repo-config-migration.yaml index 09602d4..a3addbd 100644 --- a/.github/workflows/repo-config-migration.yaml +++ b/.github/workflows/repo-config-migration.yaml @@ -24,8 +24,11 @@ on: types: [opened, synchronize, reopened, labeled, unlabeled] permissions: + # Commit migration changes back to the PR branch. contents: write + # Create and normalize migration state labels. issues: write + # Read/update pull request metadata and comments. pull-requests: write jobs: @@ -42,12 +45,20 @@ jobs: steps: - name: Generate token id: create-app-token - uses: actions/create-github-app-token@f8d387b68d61c58ab83c6c016672934102569859 # v3.0.0 + uses: actions/create-github-app-token@29824e69f54612133e76f7eaac726eef6c875baf # v2.2.1 with: app-id: ${{ secrets.FREQUENZ_AUTO_DEPENDABOT_APP_ID }} private-key: ${{ secrets.FREQUENZ_AUTO_DEPENDABOT_APP_PRIVATE_KEY }} + # Push migration commits to the PR branch. + permission-contents: write + # Manage labels when auto-merging patch-only updates. + permission-issues: write + # Approve pull requests and enable auto-merge. + permission-pull-requests: write + # Allow pushes when migration changes workflow files. + permission-workflows: write - name: Migrate - uses: frequenz-floss/gh-action-dependabot-migrate@b389f72f9282346920150a67495efbae450ac07b # v1.1.0 + uses: frequenz-floss/gh-action-dependabot-migrate@07dc7e74726498c50726a80cc2167a04d896508f # v1.0.0 with: script-url-template: >- https://raw.githubusercontent.com/frequenz-floss/frequenz-repo-config-python/{version}/cookiecutter/migrate.py diff --git a/pyproject.toml b/pyproject.toml index cbcbb63..e5e1b71 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -5,7 +5,7 @@ requires = [ "setuptools == 82.0.1", "setuptools_scm[toml] == 10.0.5", - "frequenz-repo-config[lib] == 0.14.0", + "frequenz-repo-config[lib] == 0.17.0", ] build-backend = "setuptools.build_meta" @@ -13,12 +13,12 @@ build-backend = "setuptools.build_meta" name = "frequenz-client-common" description = "Common code and utilities for Frequenz API clients" readme = "README.md" -license = { text = "MIT" } +license = "MIT" +license-files = ["LICENSE"] keywords = ["frequenz", "python", "lib", "library", "client-common"] classifiers = [ "Development Status :: 3 - Alpha", "Intended Audience :: Developers", - "License :: OSI Approved :: MIT License", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3 :: Only", "Topic :: Software Development :: Libraries", @@ -40,6 +40,7 @@ email = "floss@frequenz.com" [project.optional-dependencies] dev-flake8 = [ "flake8 == 7.3.0", + "flake8-datetimez == 20.10.0", "flake8-docstrings == 1.7.0", "flake8-pyproject == 1.2.4", # For reading the flake8 config from pyproject.toml "pydoclint == 0.8.3", @@ -56,7 +57,7 @@ dev-mkdocs = [ "mkdocs-material == 9.7.6", "mkdocstrings[python] == 1.0.3", "mkdocstrings-python == 2.0.3", - "frequenz-repo-config[lib] == 0.14.0", + "frequenz-repo-config[lib] == 0.17.0", ] dev-mypy = [ "mypy == 1.20.0", @@ -65,7 +66,7 @@ dev-mypy = [ # For checking the noxfile, docs/ script, and tests "frequenz-client-common[dev-mkdocs,dev-noxfile,dev-pytest]", ] -dev-noxfile = ["nox == 2026.2.9", "frequenz-repo-config[lib] == 0.14.0"] +dev-noxfile = ["nox == 2026.2.9", "frequenz-repo-config[lib] == 0.17.0"] dev-pylint = [ "pylint == 4.0.5", # For checking the noxfile, docs/ script, and tests @@ -73,7 +74,7 @@ dev-pylint = [ ] dev-pytest = [ "pytest == 9.0.2", - "frequenz-repo-config[extra-lint-examples] == 0.14.0", + "frequenz-repo-config[extra-lint-examples] == 0.17.0", "hypothesis == 6.151.10", "pytest-mock == 3.15.1", "pytest-asyncio == 1.3.0", diff --git a/tests/proto/test_datetime.py b/tests/proto/test_datetime.py index 0025d7f..82c6eb4 100644 --- a/tests/proto/test_datetime.py +++ b/tests/proto/test_datetime.py @@ -15,9 +15,10 @@ from frequenz.client.common.proto import datetime_from_proto, datetime_to_proto # Strategy for generating datetime objects +# It requires naive datetime objects because it creates the timezone via a strategy datetime_strategy = st.datetimes( - min_value=datetime(1970, 1, 1), - max_value=datetime(9999, 12, 31), + min_value=datetime(1970, 1, 1), # noqa: DTZ001 + max_value=datetime(9999, 12, 31), # noqa: DTZ001 timezones=st.just(timezone.utc), )