♻ Terraform AWS DSD Account

name: "♻ Terraform AWS DSD Account"

on:
  workflow_dispatch:
  pull_request:
    paths:
      - "terraform/dsd/**"
    branches:
      - main
  push:
    paths:
      - "terraform/dsd/**"
    branches: ["main"]

env:
  TERRAFORM_VERSION: "1.6.6"
  TF_VAR_github_token: ${{ secrets.GH_BOT_PAT_TOKEN }}

jobs:
  terraform:
    name: "Terraform DSD"
    runs-on: ubuntu-latest
    permissions:
      contents: read
      pull-requests: write
      id-token: write
    strategy:
      matrix:
        path:
          - "iam"
    defaults:
      run:
        working-directory: "terraform/dsd/${{ matrix.path }}"
    steps:
      - uses: actions/checkout@v4
      - uses: hashicorp/setup-terraform@v3
        with:
          terraform_version: ${{ env.TERRAFORM_VERSION }}

      - name: Configure AWS Credentials for Infra Account
        uses: aws-actions/configure-aws-credentials@v4
        with:
          role-to-assume: ${{ secrets.AWS_DSD_CREATE_IAM_ROLE_ARN }}
          aws-region: eu-west-2

      - name: Setup DSD Profile
        run: |
          aws configure set region ${{ env.AWS_REGION }} --profile dsd_profile
          aws configure set aws_access_key_id ${{ env.AWS_ACCESS_KEY_ID }} --profile dsd_profile
          aws configure set aws_secret_access_key ${{ env.AWS_SECRET_ACCESS_KEY }} --profile dsd_profile
          aws configure set aws_session_token ${{ env.AWS_SESSION_TOKEN }} --profile dsd_profile

      - name: Configure AWS Credentials for CP State Account
        uses: aws-actions/configure-aws-credentials@v4
        with:
          role-to-assume: ${{ secrets.R53_BACKUP_ROLE_ARN }}
          aws-region: eu-west-2

      - name: Post Link to Apply Pipeline
        if: github.ref == 'refs/heads/main'
        uses: mshick/add-pr-comment@v1
        with:
          repo-token: ${{ secrets.GITHUB_TOKEN }}
          repo-token-user-login: 'github-actions[bot]'
          message: |
            Your PR is applying here: https://github.com/ministryofjustice/operations-engineering/actions/workflows/cicd-terraform-dsd-iam.yml?query=event%3Apush+branch%3Amain

      - name: Terraform Format Check
        id: fmt-check
        run: terraform fmt -check

      - name: Terraform Init
        id: init
        run: ls -latr; terraform init -input=false -no-color

      - name: Terraform Validate
        id: validate
        run: terraform validate -no-color

      - name: Terraform Plan
        id: plan
        run: terraform plan -input=false -no-color

      - name: Delete Old Comments
        uses: maheshrayas/action-pr-comment-delete@v3.0
        with:
          github_token: '${{ secrets.GITHUB_TOKEN }}'
          org: ministryofjustice
          repo: operations-engineering
          user: 'github-actions[bot]'
          issue: '${{github.event.number}}'

      - name: Post Plan to GitHub PR
        if: github.ref != 'refs/heads/main'
        uses: mshick/add-pr-comment@v1
        with:
          repo-token: ${{ secrets.GITHUB_TOKEN }}
          repo-token-user-login: 'github-actions[bot]'
          message: |
            ## ${{ matrix.path }} plan
            ```
            ${{ steps.plan.outputs.stdout || steps.plan.outputs.stderr }}
            ```

      - name: Terraform Apply
        if: github.ref == 'refs/heads/main'
        id: apply
        run: terraform apply -input=false -no-color -auto-approve


Trivy Vulnerability Scan

name: Trivy Vulnerability Scan
on:
  schedule:
    - cron: "0 9 * * 1"
  workflow_dispatch:
jobs:
  check-for-vulnerabilities:
    runs-on: ubuntu-latest
    steps:
      - name: Checkout code
        uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 #v4.1.1

      - name: Run Trivy Vulnerability Scanner
        uses: aquasecurity/trivy-action@062f2592684a31eb3aa050cc61e7ca1451cecd3d #v0.18.0
        with:
          scan-type: 'fs'
          ignore-unfixed: true
          format: 'sarif'
          output: 'trivy-results.sarif'
          severity: 'CRITICAL,HIGH'

      - name: Upload Trivy scan results to GitHub Security tab
        uses: github/codeql-action/upload-sarif@v3
        with:
          sarif_file: 'trivy-results.sarif'

      - name: CVE Description escaped extraction and print
        run: |
          SCAN_RESULTS=$(jq -r '.runs[0].tool.driver.rules | map(.help.text) | join("\\n")' trivy-results.sarif)
          {
            echo "CVE_CRITICAL=$(echo "$SCAN_RESULTS" | grep -o CRITICAL | wc -l)"
            echo "CVE_HIGH=$(echo "$SCAN_RESULTS" | grep -o HIGH | wc -l)"
            echo "CVE_MEDIUM=$(echo "$SCAN_RESULTS" | grep -o MEDIUM | wc -l)"
          } >> "$GITHUB_ENV"

          echo "$SCAN_RESULTS"

      - name: Create an Issue for Detected Vulnerabilities
        id: issue-creator
        if: env.CVE_HIGH > 0 || env.CVE_CRITICAL > 0
        uses: JasonEtco/create-an-issue@1b14a70e4d8dc185e5cc76d3bec9eab20257b2c5 #v2.8.2
        env:
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
        with:
          update_existing: false
          filename: .github/ISSUE_TEMPLATE/trivy-vulnerability-template.md

      - name: Fails if CVE HIGH or CRITICAL are detected
        id: cve-threshold
        if: env.CVE_HIGH > 0 || env.CVE_CRITICAL > 0
        run: exit 1

      - name: Send notification to Slack
        id: slack
        if: always() && (github.event_name == 'schedule' || github.event_name == 'workflow_dispatch') && steps.cve-threshold.outcome == 'failure'
        uses: slackapi/slack-github-action@e28cf165c92ffef168d23c5c9000cffc8a25e117 #v1.24.0
        with:
          payload: |
            {
              "blocks": [
                {
                  "type": "header",
                  "text": {
                    "type": "plain_text",
                    "text": "Vulnerability Scan Report: ${{ github.event.repository.name }}"
                  }
                },
                {
                  "type": "section",
                  "text": {
                    "type": "mrkdwn",
                    "text": "*Scan Summary:*"
                  }
                },
                {
                  "type": "section",
                  "text": {
                    "type": "mrkdwn",
                    "text": "*CRITICAL:* :rotating_light: ${{ env.CVE_CRITICAL }}"
                  }
                },
                {
                  "type": "section",
                  "text": {
                    "type": "mrkdwn",
                    "text": "*HIGH:* :warning: ${{ env.CVE_HIGH }}"
                  }
                },
                {
                  "type": "section",
                  "text": {
                    "type": "mrkdwn",
                    "text": "*MEDIUM:* :yellow_heart: ${{ env.CVE_MEDIUM }}"
                  }
                },
                {
                  "type": "section",
                  "text": {
                    "type": "mrkdwn",
                    "text": "Please review the detailed scan results and take necessary actions.\n\n<https://github.com/${{ github.repository }}/security/code-scanning|View Details on GitHub>\n\n<https://runbooks.operations-engineering.service.justice.gov.uk/documentation/internal/dependency-alerts.html|Runbook for Responding to Dependency Alerts>\n\n*Issue Created:* <${{ steps.issue-creator.outputs.url }}|View Issue>"
                  }
                }
              ]
            }

        env:
          SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }}
          SLACK_WEBHOOK_TYPE: INCOMING_WEBHOOK


name: Alarm if remaining GitHub Actions quota is low

on:
  schedule:
    - cron: "0 4 * * *"
  workflow_dispatch:

env:
  ADMIN_SLACK_TOKEN: ${{ secrets.ADMIN_SEND_TO_SLACK }}
  GH_TOKEN: ${{ secrets.OPS_ENG_ENTERPRISE_BILLING_PAT_TOKEN }}

jobs:
  low-quota-threshold:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-python@v5
        with:
          python-version: "3.11"
          cache: "pipenv"
      - name: Install Pipenv
        run: |
          pip install pipenv
          pipenv install
      - run: pipenv run python3 -m bin.alert_on_low_github_actions_quota
      - name: Report failure to Slack
        if: always()
        uses: ravsamhq/notify-slack-action@v2
        with:
          status: ${{ job.status }}
          notify_when: "failure"
          notification_title: "Failed to check for low GitHub actions minutes"
        env:
          SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }}


🤖 Alarm for Low Gandi Funds

name: 🤖 Alarm for Low Gandi Funds

on:
  schedule:
    - cron: "0 12 * * *"
  workflow_dispatch:

jobs:
  alarm-for-low-gandi-funds:
    name: Run the check to see if more Gandi funds are needed.
    runs-on: ubuntu-latest
    permissions:
      id-token: write
      contents: read
    steps:
      - name: checkout repo content
        uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
      - name: Python Setup
        uses: actions/setup-python@82c7e631bb3cdc910f68e0081d67478d79c6982d # v5.1.0
        with:
          python-version: '3.11'
          cache: "pipenv"
      - name: Install Pipenv
        run: |
          pip install pipenv
          pipenv install
      - name: execute the low gandi funds script
        run: pipenv run python3 -m bin.alert_on_low_gandi_funds
        env:
          GANDI_FUNDS_TOKEN: ${{ secrets.GANDI_FUNDS_TOKEN }}
          GANDI_ORG_ID: ${{ secrets.GANDI_ORG_ID }}
          ADMIN_SLACK_TOKEN: ${{ secrets.ADMIN_SEND_TO_SLACK }}
      - name: Report failure to Slack
        if: always()
        uses: ravsamhq/notify-slack-action@be814b201e233b2dc673608aa46e5447c8ab13f2 # v2.5.0
        with:
          status: ${{ job.status }}
          notify_when: 'failure'
          notification_title: 'Failed GitHub Action Run'
        env:
          SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }}


🧪 Get Audit Log Users

name: 🧪 Get Audit Log Users

on:
  workflow_dispatch:

jobs:
  check-for-audit-log-users:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-python@v5
        with:
          python-version: "3.11"
          cache: "pipenv"
      - name: Install Pipenv
        run: |
          pip install pipenv
          pipenv install
      - run: pipenv run python3 -m bin.identify_dormant_github_users
        env:
          GH_ADMIN_TOKEN: ${{ secrets.GH_BOT_AUDIT_LOG_PAT_TOKEN }}


Terraform for s3 to cloudtrail integration for github logs


🤖 Generate PAT Token Report

name: 🤖 Generate PAT Token Report

on:
  schedule:
    - cron: "0 8 * * 5"

jobs:
  pat-token-report:
    runs-on: ubuntu-latest
    steps:
      - name: Generate a token to access Operations Engineering PAT App
        id: generate-token
        uses: actions/create-github-app-token@v1
        with:
          app-id: ${{ secrets.OPERATIONS_ENGINEERING_PAT_APP_ID }}
          private-key: ${{ secrets.OE_PAT_APP_PRIVATE_KEY }}
      - uses: actions/checkout@v4
      - uses: actions/setup-python@v4
        with:
          python-version: "3.11"
          cache: "pipenv"
      - name: Install Pipenv
        run: |
          pip install pipenv
          pipenv install
      - run: pipenv run python3 -m bin.generate_pat_token_report
        env:
          GH_APP_TOKEN: ${{ steps.generate-token.outputs.token }}
          ADMIN_SLACK_TOKEN: ${{ secrets.ADMIN_SEND_TO_SLACK }}


🧪 Identify Dormant GitHub Users

# This workflow is triggered manually to identify dormant GitHub users. It's currently in an experimental phase and will eventually be integrated into the main dormant user process.
name: 🧪 Identify Dormant GitHub Users

on:
  workflow_dispatch:

jobs:
  identify_dormant_github_users:
    runs-on: ubuntu-latest
    permissions:
      id-token: write
      contents: read
    steps:
      - uses: actions/checkout@v4
      - name: Configure AWS credentials
        uses: aws-actions/configure-aws-credentials@v3
        with:
          role-to-assume: ${{secrets.AWS_ID_DORMANT_USERS}}
          aws-region: eu-west-2
      - uses: actions/setup-python@v5
        with:
          python-version: "3.11"
          cache: "pipenv"
      - name: Install Pipenv
        run: |
          pip install pipenv
          pipenv install
      - run: pipenv run python3 -m bin.identify_dormant_github_users
        env:
          # GH_ADMIN_TOKEN is a GitHub personal access token that has enterprise read-only audit access.
          # This is generated by one of the GitHub enterprise owners, listed here: https://github.com/orgs/ministryofjustice/people/enterprise_owners.
          GH_ADMIN_TOKEN: ${{ secrets.OPERATIONS_ENGINEERING_GITHUB_AUDIT_LOG_READ_ONLY }}
          ADMIN_SLACK_TOKEN: ${{ secrets.ADMIN_SEND_TO_SLACK }}
          AUTH0_CLIENT_ID: ${{ secrets.AUTH0_CLIENT_ID }}
          AUTH0_CLIENT_SECRET: ${{ secrets.AUTH0_CLIENT_SECRET }}


🧪 Send Support Stats to Slack

name: 🧪 Send Support Stats to Slack

on:
  workflow_dispatch:
  push:
    paths:
      - 'data/support_stats/support_stats.csv'
jobs:
  send-support-stats-to-slack:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-python@v4
        with:
          python-version: "3.11"
          cache: "pipenv"
      - name: Install Pipenv
        run: |
          pip install pipenv
          pipenv install
      - run: pipenv run python3 -m bin.support_stats_reporting
        env:
          ADMIN_SLACK_TOKEN: ${{ secrets.ADMIN_SEND_TO_SLACK }}
          SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }}


🤖 Alarm for Unused CircleCI Contexts

name: 🤖 Alarm for Unused CircleCI Contexts

on:
  schedule:
    - cron: '0 8 1 */3 *'
  workflow_dispatch:

jobs:
  report-on-unused-circleci-contexts:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 #v4.1.1
      - uses: actions/setup-python@82c7e631bb3cdc910f68e0081d67478d79c6982d # v5.1.0
        with:
          python-version: "3.11"
          cache: "pipenv"
      - name: Install Pipenv
        run: |
          pip install pipenv
          pipenv install
      - run: pipenv run python3 -m bin.alert_on_unused_contexts
        env:
          ADMIN_GITHUB_TOKEN: ${{ secrets.GH_BOT_PAT_TOKEN }}
          ADMIN_CIRCLECI_TOKEN: ${{ secrets.ADMIN_CIRCLECI_TOKEN }}
          ADMIN_SLACK_TOKEN: ${{ secrets.SLACK_BOT_TOKEN }}
          CIRCLE_CI_OWNER_ID: ${{ secrets.CIRCLE_CI_OWNER_ID }}
      - name: Report failure to Slack
        if: always()
        uses: ravsamhq/notify-slack-action@be814b201e233b2dc673608aa46e5447c8ab13f2 # v2.5.0
        with:
          status: ${{ job.status }}
          notify_when: "failure"
          notification_title: "Failed to report on unused CircleCI contexts"
        env:
          SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }}


🧪 Experiment Get Collaborators GraphQL Query


🤖 Remove Stale Outside Collaborators

name: 🤖 Remove Stale Outside Collaborators
on:
  workflow_dispatch:
  schedule:
    - cron: "0 4 1 * *"

jobs:
  remove-stale-outside-collaborators:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
      - name: Set up
        uses: actions/setup-python@82c7e631bb3cdc910f68e0081d67478d79c6982d # v5.1.0
        with:
          python-version: "3.11"
          cache: "pipenv"
      - name: Install Pipenv
        run: |
          pip install pipenv
          pipenv install
      - name: Remove stale outside collaborators
        run: pipenv run python -m bin.remove_stale_outside_collaborators
        env:
          ADMIN_GITHUB_TOKEN: ${{ secrets.OPS_BOT_TOKEN }}
          GITHUB_ORGANIZATION_NAME: ministryofjustice
      - name: Report failure to Slack
        if: always()
        uses: ravsamhq/notify-slack-action@be814b201e233b2dc673608aa46e5447c8ab13f2 # v2.5.0
        with:
          status: ${{ job.status }}
          notify_when: "failure"
          notification_title: "Failed GitHub Action Run"
        env:
          SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }}


name: Publish Patroni distributions to PyPI and TestPyPI
on:
  push:
    tags:
      - 'v[0-9]+.[0-9]+.[0-9]+'
  release:
    types:
      - published
jobs:
  build-n-publish:
    name: Build and publish Patroni distributions to PyPI and TestPyPI
    runs-on: ubuntu-latest
    steps:
    - uses: actions/checkout@v4

    - name: Set up Python 3.11
      uses: actions/setup-python@v5
      with:
        python-version: 3.11

    - name: Install dependencies
      run: python .github/workflows/install_deps.py

    - name: Run tests and flake8
      run: python .github/workflows/run_tests.py

    - name: Install Python packaging build frontend
      run: python -m pip install build

    - name: Build a binary wheel and a source tarball
      run: python -m build

    - name: Publish distribution to Test PyPI
      if: github.event_name == 'push'
      uses: pypa/gh-action-pypi-publish@v1.8.14
      with:
        password: ${{ secrets.TEST_PYPI_API_TOKEN }}
        repository_url: https://test.pypi.org/legacy/

    - name: Publish distribution to PyPI
      if: github.event_name == 'release'
      uses: pypa/gh-action-pypi-publish@v1.8.14
      with:
        password: ${{ secrets.PYPI_API_TOKEN }}


Tests

name: Tests

on:
  pull_request:
  push:
    branches:
    - master
    - 'REL_[0-9]+_[0-9]+'

env:
  CODACY_PROJECT_TOKEN: ${{ secrets.CODACY_PROJECT_TOKEN }}
  SECRETS_AVAILABLE: ${{ secrets.CODACY_PROJECT_TOKEN != '' }}

jobs:
  unit:
    runs-on: ${{ matrix.os }}-latest
    strategy:
      fail-fast: false
      matrix:
        os: [ubuntu, windows, macos]

    steps:
    - uses: actions/checkout@v4

    - name: Set up Python 3.7
      uses: actions/setup-python@v5
      with:
        python-version: 3.7
      if: matrix.os != 'macos'
    - name: Install dependencies
      run: python .github/workflows/install_deps.py
      if: matrix.os != 'macos'
    - name: Run tests and flake8
      run: python .github/workflows/run_tests.py
      if: matrix.os != 'macos'

    - name: Set up Python 3.8
      uses: actions/setup-python@v5
      with:
        python-version: 3.8
    - name: Install dependencies
      run: python .github/workflows/install_deps.py
    - name: Run tests and flake8
      run: python .github/workflows/run_tests.py

    - name: Set up Python 3.9
      uses: actions/setup-python@v5
      with:
        python-version: 3.9
    - name: Install dependencies
      run: python .github/workflows/install_deps.py
    - name: Run tests and flake8
      run: python .github/workflows/run_tests.py

    - name: Set up Python 3.10
      uses: actions/setup-python@v5
      with:
        python-version: '3.10'
    - name: Install dependencies
      run: python .github/workflows/install_deps.py
    - name: Run tests and flake8
      run: python .github/workflows/run_tests.py

    - name: Set up Python 3.11
      uses: actions/setup-python@v5
      with:
        python-version: 3.11
    - name: Install dependencies
      run: python .github/workflows/install_deps.py
    - name: Run tests and flake8
      run: python .github/workflows/run_tests.py

    - name: Set up Python 3.12
      uses: actions/setup-python@v5
      with:
        python-version: 3.12
    - name: Install dependencies
      run: python .github/workflows/install_deps.py
    - name: Run tests and flake8
      run: python .github/workflows/run_tests.py

    - name: Combine coverage
      run: python .github/workflows/run_tests.py combine

    - name: Install coveralls
      run: python -m pip install coveralls

    - name: Upload Coverage
      env:
        COVERALLS_FLAG_NAME: unit-${{ matrix.os }}
        COVERALLS_PARALLEL: 'true'
        GITHUB_TOKEN: ${{ secrets.github_token }}
      run: python -m coveralls --service=github

  behave:
    runs-on: ${{ matrix.os }}-latest
    env:
      DCS: ${{ matrix.dcs }}
      ETCDVERSION: 3.4.23
      PGVERSION: 16.1-1  # for windows and macos
    strategy:
      fail-fast: false
      matrix:
        os: [ubuntu]
        python-version: [3.7, 3.12]
        dcs: [etcd, etcd3, consul, exhibitor, kubernetes, raft]
        include:
        - os: macos
          python-version: 3.8
          dcs: raft
        - os: macos
          python-version: 3.9
          dcs: etcd
        - os: macos
          python-version: 3.11
          dcs: etcd3

    steps:
    - uses: actions/checkout@v4
    - name: Set up Python
      uses: actions/setup-python@v5
      with:
        python-version: ${{ matrix.python-version }}
    - uses: nolar/setup-k3d-k3s@v1
      if: matrix.dcs == 'kubernetes'
    - name: Add postgresql and citus apt repo
      run: |
        sudo apt-get update -y
        sudo apt-get install -y wget ca-certificates gnupg debian-archive-keyring apt-transport-https
        sudo sh -c 'echo "deb http://apt.postgresql.org/pub/repos/apt $(lsb_release -cs)-pgdg main" > /etc/apt/sources.list.d/pgdg.list'
        sudo sh -c 'wget -qO - https://www.postgresql.org/media/keys/ACCC4CF8.asc | gpg --dearmor > /etc/apt/trusted.gpg.d/apt.postgresql.org.gpg'
        sudo sh -c 'echo "deb [signed-by=/etc/apt/trusted.gpg.d/citusdata_community.gpg] https://packagecloud.io/citusdata/community/ubuntu/ $(lsb_release -cs) main" > /etc/apt/sources.list.d/citusdata_community.list'
        sudo sh -c 'wget -qO - https://packagecloud.io/citusdata/community/gpgkey | gpg --dearmor > /etc/apt/trusted.gpg.d/citusdata_community.gpg'
      if: matrix.os == 'ubuntu'
    - name: Install dependencies
      run: python .github/workflows/install_deps.py
    - name: Run behave tests
      run: python .github/workflows/run_tests.py
    - name: Upload logs if behave failed
      uses: actions/upload-artifact@v4
      if: failure()
      with:
        name: behave-${{ matrix.os }}-${{ matrix.dcs }}-${{ matrix.python-version }}-logs
        path: |
          features/output/*_failed/*postgres?.*
          features/output/*.log
        if-no-files-found: error
        retention-days: 5
    - name: Generate coverage xml report
      run: python -m coverage xml -o cobertura.xml
    - name: Upload coverage to Codacy
      run: bash <(curl -Ls https://coverage.codacy.com/get.sh) report -r cobertura.xml -l Python --partial
      if: ${{ env.SECRETS_AVAILABLE == 'true' }}

  coveralls-finish:
    name: Finalize coveralls.io
    needs: unit
    runs-on: ubuntu-latest
    steps:
    - uses: actions/setup-python@v5
    - run: python -m pip install coveralls
    - run: python -m coveralls --service=github --finish
      env:
        GITHUB_TOKEN: ${{ secrets.github_token }}

  codacy-final:
    name: Finalize Codacy
    needs: behave
    runs-on: ubuntu-latest
    steps:
    - run: bash <(curl -Ls https://coverage.codacy.com/get.sh) final
      if: ${{ env.SECRETS_AVAILABLE == 'true' }}

  pyright:
    runs-on: ubuntu-latest
    steps:
    - uses: actions/checkout@v4

    - name: Set up Python 3.12
      uses: actions/setup-python@v5
      with:
        python-version: 3.12

    - name: Install dependencies
      run: python -m pip install -r requirements.txt psycopg2-binary psycopg

    - uses: jakebailey/pyright-action@v2
      with:
        version: 1.1.367

  docs:
    runs-on: ubuntu-latest
    steps:
    - uses: actions/checkout@v4

    - name: Set up Python 3.11
      uses: actions/setup-python@v5
      with:
        python-version: 3.11
        cache: pip

    - name: Install dependencies
      run: pip install tox

    - name: Install package dependencies
      run: |
        sudo apt update \
        && sudo apt install -y \
            latexmk texlive-latex-extra tex-gyre \
            --no-install-recommends

    - name: Generate documentation
      run: tox -m docs