frequenz-microgrid 0.2.2

A high-level interface to the Frequenz Microgrid API.
Documentation
name: CI

on:
  merge_group:
  push:
    # We need to explicitly include tags because otherwise when adding
    # `branches-ignore` it will only trigger on branches.
    tags:
      - '*'
    branches-ignore:
      # Ignore pushes to merge queues.
      # We only want to test the merge commit (`merge_group` event), the hashes
      # in the push were already tested by the PR checks
      - 'gh-readonly-queue/**'
      - 'dependabot/**'
  workflow_dispatch:

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
  DEFAULT_PYTHON_VERSION: '3.11'
  # It would be nice to be able to also define a DEFAULT_UBUNTU_VERSION
  # but sadly `env` can't be used either in `runs-on`.

jobs:
  protolint:
    name: Check proto files with protolint
    runs-on: ubuntu-24.04

    steps:
      - name: Setup Git
        uses: frequenz-floss/gh-action-setup-git@v1.0.0

      - name: Fetch sources
        uses: actions/checkout@v4
        with:
          submodules: true

      - name: Run protolint
        # Only use hashes here, as we are passing the github token, we want to
        # make sure updates are done consciously to avoid security issues if the
        # action repo gets hacked
        uses: yoheimuta/action-protolint@e62319541dc5107df5e3a5010acb8987004d3d25 # v1.3.0
        with:
          fail_on_error: true
          filter_mode: nofilter
          github_token: ${{ secrets.github_token }}
          protolint_flags: proto/
          protolint_version: "0.53.0"
          reporter: github-check

  protoc:
    name: Test with protoc
    runs-on: ubuntu-24.04
    steps:
      - name: Fetch sources
        uses: actions/checkout@v4
        with:
          submodules: true
      - name: Source protoc
        run: |
          curl -LO https://github.com/protocolbuffers/protobuf/releases/download/v25.1/protoc-25.1-linux-x86_64.zip
          mkdir protoc
          unzip protoc-25.1-linux-x86_64.zip -d protoc
          sudo cp -r protoc/bin/* /usr/bin/
          sudo cp -r protoc/include/* /usr/include/
          rm -r protoc-25.1-linux-x86_64.zip protoc
      - name: Run sanity tests
        run: |
          protoc \
          --fatal_warnings \
          -I proto \
          -I submodules/frequenz-api-common/proto \
          -I submodules/api-common-protos \
          --python_out=. \
          frequenz/api/microgrid/v1/microgrid.proto

  nox:
    name: Test with nox
    strategy:
      fail-fast: false
      matrix:
        arch:
          - amd64
          - arm
        os:
          - ubuntu-24.04
        python:
          - "3.11"
          - "3.12"
        nox-session:
          # To speed things up a bit we use the special ci_checks_max session
          # 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) || '' }}

    steps:
      - name: Run nox
        uses: frequenz-floss/gh-action-nox@v1.0.0
        with:
          python-version: ${{ matrix.python }}
          nox-session: ${{ matrix.nox-session }}

  # This job runs if all the `nox` matrix jobs ran and succeeded.
  # It is only used to have a single job that we can require in branch
  # protection rules, so we don't have to update the protection rules each time
  # we add or remove a job from the matrix.
  nox-all:
    # The job name should match the name of the `nox` job.
    name: Test with nox
    needs: ["nox"]
    # We skip this job only if nox was also skipped
    if: always() && needs.nox.result != 'skipped'
    runs-on: ubuntu-24.04
    env:
      DEPS_RESULT: ${{ needs.nox.result }}
    steps:
      - name: Check matrix job result
        run: test "$DEPS_RESULT" = "success"

  build:
    name: Build distribution packages
    # Since this is a pure Python package, we only need to build it once. If it
    # had any architecture specific code, we would need to build it for each
    # architecture.
    runs-on: ubuntu-24.04

    steps:
      - name: Setup Git
        uses: frequenz-floss/gh-action-setup-git@v1.0.0

      - name: Fetch sources
        uses: actions/checkout@v4
        with:
          submodules: true

      - name: Setup Python
        uses: frequenz-floss/gh-action-setup-python-with-deps@v1.0.0
        with:
          python-version: ${{ env.DEFAULT_PYTHON_VERSION }}
          dependencies: build

      - name: Build the source and binary distribution
        run: python -m build

      - name: Upload distribution files
        uses: actions/upload-artifact@v4
        with:
          name: dist-packages
          path: dist/
          if-no-files-found: error

  test-installation:
    name: Test package installation
    needs: ["build"]
    strategy:
      fail-fast: false
      matrix:
        arch:
          - amd64
          - arm
        os:
          - ubuntu-24.04
        python:
          - "3.11"
          - "3.12"
    runs-on: ${{ matrix.os }}${{ matrix.arch != 'amd64' && format('-{0}', matrix.arch) || '' }}

    steps:
      - name: Setup Git
        uses: frequenz-floss/gh-action-setup-git@v1.0.0

      - name: Print environment (debug)
        run: env

      - name: Download package
        uses: actions/download-artifact@v4
        with:
          name: dist-packages
          path: dist

      # This is necessary for the `pip` caching in the setup-python action to work
      - name: Fetch the pyproject.toml file for this action hash
        env:
          GH_TOKEN: ${{ github.token }}
          REPO: ${{ github.repository }}
          REF: ${{ github.sha }}
        run: |
          set -ux
          gh api \
              -X GET \
              -H "Accept: application/vnd.github.raw" \
              "/repos/$REPO/contents/pyproject.toml?ref=$REF" \
            > pyproject.toml

      - name: Setup Python
        uses: frequenz-floss/gh-action-setup-python-with-deps@v1.0.0
        with:
          python-version: ${{ matrix.python }}
          dependencies: dist/*.whl

      - name: Print installed packages (debug)
        run: python -m 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
  # protection rules, so we don't have to update the protection rules each time
  # we add or remove a job from the matrix.
  test-installation-all:
    # The job name should match the name of the `test-installation` job.
    name: Test package installation
    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
    env:
      DEPS_RESULT: ${{ needs.test-installation.result }}
    steps:
      - name: Check matrix job result
        run: test "$DEPS_RESULT" = "success"

  test-docs:
    name: Test documentation website generation
    if: github.event_name != 'push'
    runs-on: ubuntu-24.04
    steps:
      - name: Setup Git
        uses: frequenz-floss/gh-action-setup-git@v1.0.0

      - name: Fetch sources
        uses: actions/checkout@v4
        with:
          submodules: true

      - name: Setup Python
        uses: frequenz-floss/gh-action-setup-python-with-deps@v1.0.0
        with:
          python-version: ${{ env.DEFAULT_PYTHON_VERSION }}
          dependencies: .[dev-mkdocs]

      - name: Generate the documentation
        env:
          MIKE_VERSION: gh-${{ github.job }}
        run: |
          mike deploy $MIKE_VERSION
          mike set-default $MIKE_VERSION

      - name: Upload site
        uses: actions/upload-artifact@v4
        with:
          name: docs-site
          path: site/
          if-no-files-found: error

  publish-docs:
    name: Publish documentation website to GitHub pages
    needs: ["nox-all", "test-installation-all"]
    if: github.event_name == 'push'
    runs-on: ubuntu-24.04
    permissions:
      contents: write
    steps:
      - name: Setup Git
        uses: frequenz-floss/gh-action-setup-git@v1.0.0

      - name: Fetch sources
        uses: actions/checkout@v4
        with:
          submodules: true

      - name: Setup Python
        uses: frequenz-floss/gh-action-setup-python-with-deps@v1.0.0
        with:
          python-version: ${{ env.DEFAULT_PYTHON_VERSION }}
          dependencies: .[dev-mkdocs]

      - name: Calculate and check version
        id: mike-version
        env:
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
          GITHUB_REPO: ${{ github.repository }}
          GIT_REF: ${{ github.ref }}
          GIT_SHA: ${{ github.sha }}
        run: |
          python -m frequenz.repo.config.cli.version.mike.info

      - name: Fetch the gh-pages branch
        if: steps.mike-version.outputs.version
        run: git fetch origin gh-pages --depth=1

      - name: Build site
        if: steps.mike-version.outputs.version
        env:
          VERSION: ${{ steps.mike-version.outputs.version }}
          TITLE: ${{ steps.mike-version.outputs.title }}
          ALIASES: ${{ steps.mike-version.outputs.aliases }}
          # This is not ideal, we need to define all these variables here
          # because we need to calculate all the repository version information
          # to be able to show the correct versions in the documentation when
          # building it.
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
          GITHUB_REPO: ${{ github.repository }}
          GIT_REF: ${{ github.ref }}
          GIT_SHA: ${{ github.sha }}
        run: |
          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
          git commit -a -m "Sort versions.json"

      - name: Publish site
        if: steps.mike-version.outputs.version
        run: |
          git push origin gh-pages

  create-github-release:
    name: Create GitHub release
    needs: ["publish-docs"]
    # 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
      contents: write
      discussions: write
    runs-on: ubuntu-24.04
    steps:
      - name: Download distribution files
        uses: actions/download-artifact@v4
        with:
          name: dist-packages
          path: dist

      - name: Download RELEASE_NOTES.md
        run: |
          set -ux
          gh api \
              -X GET \
              -f ref=$REF \
              -H "Accept: application/vnd.github.raw" \
              "/repos/$REPOSITORY/contents/RELEASE_NOTES.md" \
            > RELEASE_NOTES.md
        env:
          REF: ${{ github.ref }}
          REPOSITORY: ${{ github.repository }}
          GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}

      - name: Create GitHub release
        run: |
          set -ux
          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 \
            dist/*
        env:
          REF_NAME: ${{ github.ref_name }}
          REPOSITORY: ${{ github.repository }}
          GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}

  publish-to-pypi:
    name: Publish packages to PyPI
    needs: ["create-github-release"]
    runs-on: ubuntu-24.04
    permissions:
      # For trusted publishing. See:
      # https://blog.pypi.org/posts/2023-04-20-introducing-trusted-publishers/
      id-token: write
    steps:
      - name: Download distribution files
        uses: actions/download-artifact@v4
        with:
          name: dist-packages
          path: dist

      - name: Publish the Python distribution to PyPI
        uses: pypa/gh-action-pypi-publish@release/v1