HACS Action

name: HACS Action

on:
  push:
  pull_request:
  schedule:
    - cron: "0 0 * * *"

jobs:
  hacs:
    name: HACS Action
    runs-on: "ubuntu-latest"
    steps:
      - uses: "actions/checkout@v4"
      - name: HACS Action
        uses: "hacs/action@main"
        with:
          category: "integration"


Validate with hassfest

name: Validate with hassfest

on:
  push:
  pull_request:
  schedule:
    - cron: "0 0 * * *"
  workflow_dispatch:

jobs:
  validate:
    runs-on: "ubuntu-latest"
    steps:
        - name: "Checkout the repository"
          uses: "actions/checkout@v4"

        - name: "Run hassfest validation"
          uses: "home-assistant/actions/hassfest@master"


Lint

name: Lint

on:
  push:
  pull_request:

jobs:
  lint:
    runs-on: ubuntu-latest
    strategy:
      matrix:
        python-version:
          - "3.12"

    steps:
    - name: Check out repository
      uses: actions/checkout@v4
      with:
        fetch-depth: 2

    - name: Set up Python ${{ matrix.python-version }}
      uses: actions/setup-python@v5
      with:
        python-version: ${{ matrix.python-version }}

    - name: Install dependencies
      run: |
        python -m pip install --upgrade pip
        pip install -r .github/requirements-test.txt

    - name: Check with Black Code Formatter
      run: |
        black custom_components/husqvarna_automower --check
    - name: Lint with pydocstyle
      run: |
        pydocstyle custom_components/husqvarna_automower
    - name: Lint with isort
      run: |
        isort custom_components/husqvarna_automower


PyTest

# .github/workflows/app.yaml
name: PyTest

on:
  push:
  pull_request:
  workflow_dispatch:

jobs:
  test:
    runs-on: ubuntu-latest
    timeout-minutes: 10

    steps:
      - name: Check out repository code
        uses: actions/checkout@v4

      # Setup Python (faster than using Python container)
      - name: Setup Python
        uses: actions/setup-python@v5
        with:
          python-version: "3.12"

      - name: Install pipenv
        run: |
          python -m pip install --upgrade pipenv wheel
      - id: cache-pipenv
        uses: actions/cache@v4
        with:
          path: ~/.local/share/virtualenvs
          key: ${{ runner.os }}-pipenv-pytest-${{ hashFiles('**/Pipfile.lock') }}
      - name: Install dependencies
        run: |
          pipenv install --deploy --dev
      - name: Run test suite
        run: |
          pip install -r requirements.txt
          pytest --asyncio-mode=auto custom_components/husqvarna_automower --cov=custom_components.husqvarna_automower --cov-report xml -v
      - name: Upload coverage to Codecov
        uses: codecov/codecov-action@v4
        with:
            token: ${{ secrets.CODECOV_TOKEN }}


Release Drafter

name: Release Drafter

on:
  push:
    branches:
      - main

jobs:
  release-drafter:
    name: Release Drafter
    runs-on: ubuntu-latest
    steps:
      - name: 📥 Checkout the repository
        uses: actions/checkout@v4
        with:
          fetch-depth: 0

      - name: ⏭ Get next version
        id: version
        run: |
          declare -i newpost
          latest=$(git describe --tags $(git rev-list --tags --max-count=1))
          latestpre=$(echo "$latest" | awk '{split($0,a,"."); print a[1] "." a[2]}')
          datepre=$(date --utc '+%Y.%-m')
          if [[ "$latestpre" == "$datepre" ]]; then
              latestpost=$(echo "$latest" | awk '{split($0,a,"."); print a[3]}')
              newpost=$latestpost+1
          else
              newpost=0
          fi
          echo Current version:    $latest
          echo New target version: $datepre.$newpost
          echo "::set-output name=version::$datepre.$newpost"
      - name: 🏃 Run Release Drafter
        uses: release-drafter/release-drafter@v6
        with:
          tag: ${{ steps.version.outputs.version }}
          name: ${{ steps.version.outputs.version }}
        env:
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}


Release Workflow

# This GitHub action workflow is ment to be copyable to any repo that have the same structure.
# - Your integration exsist under custom_components/{INTEGRATION_NAME}/[integration files]
# - You are using GitHub releases to publish new versions
# - You have a INTEGRATION_VERSION constant in custom_components/{INTEGRATION_NAME}/const.py

name: Release Workflow

on:
  release:
    types: [published]

jobs:
  release:
    name: Release
    runs-on: ubuntu-latest
    steps:
      - name: 📥 Checkout the repository
        uses: actions/checkout@v4

      - name: 🔢 Get release version
        id: version
        uses: home-assistant/actions/helpers/version@master

      - name: ℹ Get integration information
        id: information
        run: |
          name=$(find custom_components/ -type d -maxdepth 1 | tail -n 1 | cut -d "/" -f2)
          echo "::set-output name=name::$name"
      - name: 🖊 Set version number
        run: |
          sed -i '/INTEGRATION_VERSION = /c\INTEGRATION_VERSION = "${{ steps.version.outputs.version }}"' \
            "${{ github.workspace }}/custom_components/${{ steps.information.outputs.name }}/const.py"
          jq '.version = "${{ steps.version.outputs.version }}"' \
            "${{ github.workspace }}/custom_components/${{ steps.information.outputs.name }}/manifest.json" > tmp \
            && mv -f tmp "${{ github.workspace }}/custom_components/${{ steps.information.outputs.name }}/manifest.json"
      - name: 👀 Validate data
        run: |
          if ! grep -q 'INTEGRATION_VERSION = "${{ steps.version.outputs.version }}"' ${{ github.workspace }}/custom_components/${{ steps.information.outputs.name }}/const.py; then
            echo "The version in custom_components/${{ steps.information.outputs.name }}/const.py was not correct"
            cat ${{ github.workspace }}/custom_components/${{ steps.information.outputs.name }}/const.py | grep INTEGRATION_VERSION
            exit 1
          fi
          manifestversion=$(jq -r '.version' ${{ github.workspace }}/custom_components/${{ steps.information.outputs.name }}/manifest.json)
          if [ "$manifestversion" != "${{ steps.version.outputs.version }}" ]; then
            echo "The version in custom_components/${{ steps.information.outputs.name }}/manifest.json was not correct"
            echo "$manifestversion"
            exit 1
          fi
      - name: 📦 Create zip file for the integration
        run: |
          cd "${{ github.workspace }}/custom_components/${{ steps.information.outputs.name }}"
          zip ${{ steps.information.outputs.name }}.zip -r ./
      - name: 📤 Upload the zipfile as a release asset
        uses: actions/upload-release-asset@v1
        env:
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
        with:
          upload_url: ${{ github.event.release.upload_url }}
          asset_path: "${{ github.workspace }}/custom_components/${{ steps.information.outputs.name }}/${{ steps.information.outputs.name }}.zip"
          asset_name: ${{ steps.information.outputs.name }}.zip
          asset_content_type: application/zip


Documentation

# creates the documentation on pushes it to the gh-pages branch
name: Documentation

on:
  push:
    branches: [main]
  pull_request:
    branches: [main]

jobs:
  build:
    runs-on: ubuntu-latest
    concurrency: documentation
    steps:
      - name: Checkout
        uses: actions/checkout@v3
        with:
          fetch-depth: 0 # otherwise, you will failed to push refs to dest repo
          token: ${{ secrets.PAT }}

      - name: Install dependencies
        shell: bash
        run: |
          python -m pip install --upgrade pip
          pip install ".[docs]"

      - name: Build and Commit
        uses: sphinx-notes/pages@2.1
        with:
          documentation_path: docs
          sphinx_version: "5.3.0"

      - name: Push changes
        if: ${{ github.event_name == 'push' }}
        uses: ad-m/github-push-action@v0.6.0
        with:
          github_token: ${{ secrets.PAT }}
          branch: gh-pages


Release

# This workflow will
# 1) Update the version number in pyproject.toml based on the commit history
# 2) Create a git tag
# 3) Create a release on GitHub
# 4) Upload the package to PyPI

name: Release
on:
  push:
    branches: [main]
  workflow_run:
    workflows: ["tests"]
    types:
      - completed

jobs:
  release:
    runs-on: ubuntu-latest
    concurrency: release
    permissions:
      id-token: write  # IMPORTANT: this permission is mandatory for trusted publishing using PyPI
      # a guide on how to set it up is available here: https://blog.pypi.org/posts/2023-04-20-introducing-trusted-publishers/

    if: ${{ github.ref == 'refs/heads/main' && github.event.workflow_run.conclusion == 'success'}}
    steps:
      # Checkout action is required for token to persist
      - uses: actions/checkout@v3
        with:
          fetch-depth: 0
          token: ${{ secrets.PAT }}

      - name: Python Semantic Release
        uses: python-semantic-release/python-semantic-release@v8.0.4
        id: release
        with:
          github_token: ${{ secrets.PAT }}

      - name: Publish package distributions to PyPI
        uses: pypa/gh-action-pypi-publish@release/v1
        if: steps.release.outputs.released == 'true'
        # This action supports PyPI's trusted publishing implementation, which allows authentication to PyPI without a manually
        # configured API token or username/password combination. To perform trusted publishing with this action, your project's
        # publisher must already be configured on PyPI.

      - name: Publish package distributions to GitHub Releases
        uses: python-semantic-release/upload-to-gh-release@main
        if: steps.release.outputs.released == 'true'
        with:
          github_token: ${{ secrets.PAT }}


Tests

# This workflow will:
# 1) install Python dependencies
# 2) run make test

name: Tests
on:
  push:
    branches: [main]
  pull_request:
    branches: [main]

jobs:
  pytest:
    runs-on: ${{ matrix.os }}
    permissions:
      contents: read
      issues: read
      checks: write
      pull-requests: write
    strategy:
      matrix:
        os: [ubuntu-latest, macos-latest, windows-latest]
        python-version: ["3.9", "3.10"]

    # This allows a subsequently queued workflow run to interrupt previous runs
    concurrency:
      group: "${{ github.workflow }}-${{ matrix.python-version}}-${{ matrix.os }} @ ${{ github.ref }}"
      cancel-in-progress: true

    steps:
      - uses: actions/checkout@v3

      - name: Set up Python
        uses: actions/setup-python@v4
        with:
          python-version: ${{ matrix.python-version }}
          cache: 'pip' # caching pip dependencies

      - name: Install dependencies
        shell: bash
        run: |
          make install

      - name: Run tests
        shell: bash
        run: |
          make test


name: static_type_checks

on:
  pull_request:
    branches: [main]
  push:
    branches: [main]

jobs:
  static_type_checks:
    runs-on: ubuntu-latest
    strategy:
      matrix:
        os: [ubuntu-latest]
        python-version: ["3.9"]
    steps:
      - uses: actions/checkout@v3

      - name: Set up Python
        uses: actions/setup-python@v4
        id: setup_python
        with:
          python-version: ${{ matrix.python-version}}
          cache: pip

      - name: Install dependencies
        shell: bash
        run: |
          make install

      - name: Run static type checker
        shell: bash
        run: |
          make static-type-check


run-linting

# GitHub action to run linting

name: run-linting

on:
  pull_request:
    branches: [main]
  push:
    branches: [main]

jobs:
  pre-commit:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3

      - uses: actions/setup-python@v4
        with:
          python-version: "3.9"
          cache: "pip"

      - name: Install dependencies
        run: make install

      - name: Lint
        id: lint
        run: |
          make lint


Close stale issues and PRs

name: "Close stale issues and PRs"
on:
  schedule:
    - cron: "0 13 * * 2-5"
  workflow_dispatch:

jobs:
  stale:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/stale@v6
        with:
          days-before-issue-stale: 32
          days-before-issue-close: 7
          stale-issue-message: "This issue is stale because it has been open for 14 days with no activity. Feel free to either 1) remove the stale label or 2) comment. If nothing happens, this will be closed in 7 days."
          close-issue-message: "This issue was closed automatically. Feel free to re-open it if it's important."
          days-before-pr-stale: 2
          stale-pr-message: "This PR is stale because it has been open 1+ days with no activity. Feel free to either 1) remove the stale label or 2) comment. If nothing happens, this will be closed in 7 days."
          days-before-pr-close: 7
          close-pr-message: "This PR was closed automatically. Feel free to re-open it if you still want to work on it."
          close-pr-label: "closed-by-stalebot"
          operations-per-run: 20
          exempt-pr-labels: "no-stale"
          exempt-issue-labels: "no-stale"


Auto Documentation

name: Auto Documentation

on:
 release:
    types: [published]

jobs:
  generate_docs:
    if: ${{ github.repository_owner == 'bczsalba' }} # prevents workflow from running in forked repos
    runs-on: ubuntu-latest
    steps:
      - name: Checkout master branch
        uses: actions/checkout@v2

      - name: Set up Python
        uses: actions/setup-python@v2
        with:
          python-version: "3.x"

      - name: Install pdoc & dependencies
        run: |
          python -m pip install --upgrade pip
          pip install pdoc
          pip install .

      - name: Generate documentation
        run: |
          pdoc -o ./docs pytermgui --logo https://github.com/bczsalba/pytermgui/blob/master/assets/title.png?raw=true --docformat google

      - name: Commit changes
        run: |
          git config --local user.email "41898282+github-actions[bot]@users.noreply.github.com"
          git config --local user.name "github-actions[bot]"
          git add ./docs/
          git commit -m "Auto-update documentation"

      - name: Push changes
        uses: ad-m/github-push-action@master


Generate Documentation

name: Generate Documentation

on:
  workflow_dispatch:
    inputs:
      message:
        description: Commit message

jobs:
  generate_docs:
    runs-on: ubuntu-latest
    steps:
      - name: Checkout master branch
        uses: actions/checkout@v2

      - name: Set up Python
        uses: actions/setup-python@v2
        with:
          python-version: "3.x"

      - name: Install pdoc
        run: |
          python -m pip install --upgrade pip
          pip install .
          pip install pdoc

      - name: Generate documentation
        run: |
          pdoc -o ./docs pytermgui --logo https://github.com/bczsalba/pytermgui/blob/master/assets/title.png?raw=true --docformat google

      - name: Commit changes
        run: |
          git config --local user.email "41898282+github-actions[bot]@users.noreply.github.com"
          git config --local user.name "github-actions[bot]"
          git add ./docs/
          git commit -m "${{ github.event.inputs.message }}"

      - name: Push changes
        uses: ad-m/github-push-action@master


Run linting

name: Run linting

on:
  workflow_dispatch:
  push:
    paths:
      - "pytermgui/**"

jobs:
  build:
    runs-on: ubuntu-latest
    strategy:
      matrix:
        python-version: ["3.8", "3.9", "3.10"]
    steps:
      - uses: actions/checkout@v2

      - name: Set up Python ${{ matrix.python-version }}
        uses: actions/setup-python@v2
        with:
          python-version: ${{ matrix.python-version }}

      - name: Install dependencies
        run: |
          python -m pip install --upgrade pip

          pip install .
          pip install pylint

      - name: Analysing the code with pylint
        run: |
          pylint --fail-under 10.0 -d fixme pytermgui


Run tests

name: Run tests

on:
  workflow_dispatch:
  push:
    paths:
      - "pytermgui/**"
      - "tests/**"

jobs:
  build:
    runs-on: ${{ matrix.os }}

    strategy:
      matrix:
        os: [ubuntu-latest, windows-latest, macos-latest]
        python-version: ["3.8", "3.9", "3.10"]

    steps:
      - uses: actions/checkout@v2

      - name: Set up Python ${{ matrix.python-version }}
        uses: actions/setup-python@v4
        with:
          python-version: ${{ matrix.python-version }}

      - name: Install dependencies
        run: |
          python -m pip install --upgrade pip

          pip install .

          pip install testfixtures
          pip install pytest-cov
          pip install coverage
          pip install pyyaml

      - name: Run tests with coverage
        run: |
          make test


Benchmarks

name: Benchmarks

on:
  pull_request:
    branches:
      - master
    paths-ignore:
      - 'docs/**'
      - 'devtools/**'
  workflow_dispatch:
    inputs:
      debug_enabled:
        type: boolean
        description: '(https://github.com/marketplace/actions/debugging-with-tmate)'
        required: false
        default: false

concurrency:
  group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
  cancel-in-progress: true

jobs:
  benchmark:
    runs-on: ubuntu-latest

    steps:
      # Enable tmate debugging of manually-triggered workflows if the input option was provided
      - name: Setup tmate session
        uses: mxschmitt/action-tmate@v3
        if: ${{ github.event_name == 'workflow_dispatch' && inputs.debug_enabled }}
      - uses: actions/checkout@v4
        with:
          ref: ${{ github.event.pull_request.head.sha }}
      - name: Set up Python 3.9
        uses: actions/setup-python@v5
        with:
          python-version: 3.9
      - name: Install dependencies
        run: |
          python -m pip install --upgrade pip
          pip install -r devtools/dev-requirements.txt
      - name: Benchmark with pytest-benchmark
        run: |
          pwd
          lscpu
          cd tests/benchmarks
          python -m pytest benchmark_cpu_small.py -vv \
            --benchmark-save='Latest_Commit' \
            --durations=0 \
            --benchmark-save-data
      - name: Checkout current master
        uses: actions/checkout@v4
        with:
          ref: master
          clean: false
      - name: Checkout benchmarks from PR head
        run: git checkout ${{ github.event.pull_request.head.sha }} -- tests/benchmarks
      - name: Benchmark with pytest-benchmark
        run: |
          pwd
          lscpu
          cd tests/benchmarks
          python -m pytest benchmark_cpu_small.py -vv \
            --benchmark-save='master' \
            --durations=0 \
            --benchmark-save-data
      - name: put benchmark results in same folder
        run: |
          pwd
          cd tests/benchmarks
          find .benchmarks/ -type f -printf "%T@ %p\n" | sort -n | cut -d' ' -f 2- | tail -n 1 > temp1
          find .benchmarks/ -type f -printf "%T@ %p\n" | sort -n | cut -d' ' -f 2- | tail -n 2 | head -n 1 > temp2
          t1=$(cat temp1)
          t2=$(cat temp2)
          mkdir compare_results
          cp $t1 compare_results
          cp $t2 compare_results
      - name: Compare latest commit results to the master branch results
        run: |
          pwd
          cd tests/benchmarks
          python compare_bench_results.py
          cat commit_msg.txt
      - name: comment PR with the results
        uses: thollander/actions-comment-pull-request@v2
        env:
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
        with:
          filePath: tests/benchmarks/commit_msg.txt
          comment_tag: benchmark
      - name: Upload benchmark data
        if: always()
        uses: actions/upload-artifact@v4
        with:
          name: benchmark_artifact
          path: tests/benchmarks/.benchmarks


Black formatting

name: Black formatting

on: [pull_request, workflow_dispatch]

jobs:
  black_format:

    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - name: Set up Python 3.10
        uses: actions/setup-python@v5
        with:
          python-version: '3.10'
      - name: Install dependencies
        run: |
          python -m pip install --upgrade pip
          pip install -r devtools/dev-requirements.txt
      - name: Check files using the black formatter
        run: |
          black --version
          black --check desc/ tests/ || black_return_code=$?
          echo "BLACK_RETURN_CODE=$black_return_code" >> $GITHUB_ENV
          black desc/ tests/
      - name: Annotate diff changes using reviewdog
        uses: reviewdog/action-suggester@v1
        with:
          tool_name: blackfmt
      - name: Fail if not formatted
        run: |
          exit ${{ env.BLACK_RETURN_CODE }}


Dependency test JAX

name: Dependency test JAX

on:
  pull_request:
    types: [labeled]
  workflow_dispatch:

jobs:
  jax_tests:
    if: ${{ github.event.label.name == 'test_jax' && github.event_name == 'pull_request' || github.event_name == 'workflow_dispatch' }}
    runs-on: ubuntu-latest
    strategy:
      fail-fast: false
      matrix:
        jax-version: [0.3.0,  0.3.1,  0.3.2,  0.3.3,  0.3.4,  0.3.5,
                      0.3.6,  0.3.7,  0.3.8,  0.3.9,  0.3.10, 0.3.11,
                      0.3.12, 0.3.13, 0.3.14, 0.3.15, 0.3.16, 0.3.17,
                      0.3.19, 0.3.20, 0.3.21, 0.3.22, 0.3.23, 0.3.24,
                      0.3.25, 0.4.1,  0.4.2,  0.4.3,  0.4.4,  0.4.5,
                      0.4.6,  0.4.7,  0.4.8,  0.4.9,  0.4.10, 0.4.11,
                      0.4.12, 0.4.13, 0.4.14, 0.4.16, 0.4.17, 0.4.18,
                      0.4.18, 0.4.19, 0.4.20, 0.4.21, 0.4.22, 0.4.23,
                      0.4.24, 0.4.25]
        group: [1, 2]
    steps:
      - uses: actions/checkout@v4
      - name: Set up Python 3.9
        uses: actions/setup-python@v5
        with:
          python-version: 3.9
          cache: pip
      - name: Install dependencies
        run: |
          python -m pip install --upgrade pip
          pip install -r devtools/dev-requirements.txt
          pip install matplotlib==3.5.0
      - name: Remove jax
        run: |
          pip uninstall jax jaxlib -y
      - name: install jax
        run: |
          pip install "jax[cpu]==${{ matrix.jax-version }}"
      - name: Test with pytest
        run: |
          pwd
          lscpu
          python -m pytest -m unit \
            --durations=0 \
            --mpl \
            --maxfail=1 \
            --splits 3 \
            --group ${{ matrix.group }} \
            --splitting-algorithm least_duration


Linting

name: Linting

on: [pull_request, workflow_dispatch]

jobs:
  flake8_linting:
    runs-on: ubuntu-latest
    name: Linting
    steps:
      - uses: actions/checkout@v4
      - name: Set up Python 3.10
        uses: actions/setup-python@v5
        with:
          python-version: '3.10'
      - name: Install dependencies
        run: |
          python -m pip install --upgrade pip
          pip install -r devtools/dev-requirements.txt
      - name: flake8 Lint
        uses: reviewdog/action-flake8@v3
        with:
          github_token: ${{ secrets.GITHUB_TOKEN }}
          reporter: github-pr-review


Dependecy test Matplotlib

name: Dependecy test Matplotlib

on:
  pull_request:
    types: [labeled]
  workflow_dispatch:

jobs:
  mpl_tests:
    if: ${{ github.event.label.name == 'test_mpl' && github.event_name == 'pull_request' || github.event_name == 'workflow_dispatch' }}
    runs-on: ubuntu-latest
    strategy:
      fail-fast: false
      matrix:
        mpl-version: [3.0.0]
        include:
          - mpl-version: 3.0.0
          - mpl-version: 3.0.1
          - mpl-version: 3.0.2
          - mpl-version: 3.0.3
          - mpl-version: 3.1.0
          - mpl-version: 3.1.1
          - mpl-version: 3.1.2
          - mpl-version: 3.1.3
          - mpl-version: 3.2.0
          - mpl-version: 3.2.1
          - mpl-version: 3.2.2
          - mpl-version: 3.3.0
          - mpl-version: 3.3.1
          - mpl-version: 3.3.2
          - mpl-version: 3.3.3
          - mpl-version: 3.3.4
          - mpl-version: 3.4.0
          - mpl-version: 3.4.1
          - mpl-version: 3.4.2
          - mpl-version: 3.4.3
          - mpl-version: 3.5.0
          - mpl-version: 3.5.1
          - mpl-version: 3.5.2
          - mpl-version: 3.5.3
          - mpl-version: 3.6.0
          - mpl-version: 3.6.1
          - mpl-version: 3.6.2
          - mpl-version: 3.6.3
          - mpl-version: 3.7.0
          - mpl-version: 3.7.1
          - mpl-version: 3.7.2
    steps:
      - uses: actions/checkout@v4
      - name: Set up Python 3.10
        uses: actions/setup-python@v5
        with:
          python-version: '3.10'
          cache: pip
      - name: Install dependencies
        run: |
          python -m pip install --upgrade pip
          pip install -r devtools/dev-requirements.txt
          pip install "matplotlib==${{ matrix.mpl-version }}"
      - name: Test with pytest
        run: |
          pwd
          lscpu
          python -m pytest tests/test_plotting.py --durations=0 --mpl --maxfail=1


Notebook tests

name: Notebook tests

on:
  push:
    branches:
      - master
      - dev
  pull_request:
    branches:
      - master
  workflow_dispatch:

concurrency:
  group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
  cancel-in-progress: true

jobs:
  notebook_tests:

    runs-on: ubuntu-latest
    strategy:
      matrix:
        python-version: ['3.10']
        group: [1, 2]

    steps:
      - uses: actions/checkout@v4
      - name: Set up Python ${{ matrix.python-version }}
        uses: actions/setup-python@v5
        with:
          python-version: ${{ matrix.python-version }}
      - name: Install dependencies
        run: |
          python -m pip install --upgrade pip
          pip install -r devtools/dev-requirements.txt
      - name: Test notebooks with pytest and nbmake
        run: |
          pwd
          lscpu
          export PYTHONPATH=$(pwd)
          pytest -v --nbmake "./docs/notebooks" \
            --nbmake-timeout=1000 \
            --ignore=./docs/notebooks/zernike_eval.ipynb \
            --splits 2 \
            --group ${{ matrix.group }} \


Regression tests

name: Regression tests

on:
  push:
    branches:
      - master
      - dev
  pull_request:
    branches:
      - master
  workflow_dispatch:

concurrency:
  group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
  cancel-in-progress: true

jobs:
  regression_tests:

    runs-on: ubuntu-latest
    strategy:
      matrix:
        python-version: ['3.10']
        group: [1, 2, 3, 4, 5, 6]

    steps:
      - uses: actions/checkout@v4
      - name: Set up Python ${{ matrix.python-version }}
        uses: actions/setup-python@v5
        with:
          python-version: ${{ matrix.python-version }}
      - name: Install dependencies
        run: |
          python -m pip install --upgrade pip
          pip install -r devtools/dev-requirements.txt
          pip install matplotlib==3.7.2
      - name: Set Swap Space
        uses: pierotofy/set-swap-space@master
        with:
          swap-size-gb: 10
      - name: Test with pytest
        run: |
          pwd
          lscpu
          python -m pytest -v -m regression \
            --durations=0 \
            --cov-report xml:cov.xml \
            --cov-config=setup.cfg \
            --cov=desc/ \
            --mpl \
            --mpl-results-path=mpl_results.html \
            --mpl-generate-summary=html \
            --splits 6 \
            --group ${{ matrix.group }} \
            --splitting-algorithm least_duration \
            --db ./prof.db
      - name: save coverage file and plot comparison results
        if: always()
        uses: actions/upload-artifact@v4
        with:
          name: regression_test_artifact-${{ matrix.python-version }}-${{ matrix.group }}
          path: |
            ./cov.xml
            ./mpl_results.html
            ./prof.db
      - name: Upload coverage
        id : codecov
        uses: Wandalen/wretry.action@v1.3.0
        with:
          action: codecov/codecov-action@v3
          with: |
            token: ${{ secrets.CODECOV_TOKEN }}
            name: codecov-umbrella
            files: ./cov.xml
            fail_ci_if_error: true
            verbose: true
          attempt_limit: 10
          attempt_delay: 60000 # ms, 1 min


Upload Python Package

name: Upload Python Package

on:
  release:
    types: [published]

jobs:
  deploy:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - name: Set up Python
        uses: actions/setup-python@v5
        with:
          python-version: '3.10'
      - name: Install dependencies
        run: |
          python -m pip install --upgrade pip
          pip install -r devtools/dev-requirements.txt
      - name: Build package
        run: python -m build --sdist --wheel
      - name: Publish package
        uses: pypa/gh-action-pypi-publish@27b31702a0e7fc50959f5ad993c78deac1bdfc29
        with:
          user: __token__
          password: ${{ secrets.PYPI_API_TOKEN }}


Weekly tests

name: Weekly tests

on:
  schedule:
      - cron: '17 9 * * 1'
  workflow_dispatch:

jobs:
  unit_tests:

    runs-on: ubuntu-latest
    strategy:
      matrix:
        combos: [{group: 1, python_version: '3.8'},
                 {group: 2, python_version: '3.9'},
                 {group: 3, python_version: '3.10'},
                 {group: 4, python_version: '3.11'},
                 {group: 5, python_version: '3.12'}]

    steps:
      - uses: actions/checkout@v4
      - name: Set up Python ${{ matrix.combos.python_version }}
        uses: actions/setup-python@v5
        with:
          python-version: ${{ matrix.combos.python_version }}
      - name: Install dependencies
        run: |
          python -m pip install --upgrade pip
          pip install -r devtools/dev-requirements.txt
      - name: Set Swap Space
        uses: pierotofy/set-swap-space@master
        with:
          swap-size-gb: 10
      - name: Test with pytest
        run: |
          pwd
          lscpu
          python -m pytest -v -m unit \
            --durations=0 \
            --splits 5 \
            --group ${{ matrix.combos.group }} \
            --splitting-algorithm least_duration


Unit tests

name: Unit tests

on:
  push:
    branches:
      - master
      - dev
  pull_request:
    branches:
      - master
  workflow_dispatch:

concurrency:
  group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
  cancel-in-progress: true

jobs:
  unit_tests:

    runs-on: ubuntu-latest
    strategy:
      matrix:
        combos: [{group: 1, python_version: '3.9'},
                 {group: 2, python_version: '3.10'},
                 {group: 3, python_version: '3.11'},
                 {group: 4, python_version: '3.12'}]

    steps:
      - uses: actions/checkout@v4
      - name: Set up Python ${{ matrix.combos.python_version }}
        uses: actions/setup-python@v5
        with:
          python-version: ${{ matrix.combos.python_version }}
      - name: Install dependencies
        run: |
          python -m pip install --upgrade pip
          pip install -r devtools/dev-requirements.txt
          pip install matplotlib==3.7.2
      - name: Set Swap Space
        uses: pierotofy/set-swap-space@master
        with:
          swap-size-gb: 10
      - name: Test with pytest
        run: |
          pwd
          lscpu
          python -m pytest -v -m unit \
            --durations=0 \
            --cov-report xml:cov.xml \
            --cov-config=setup.cfg \
            --cov=desc/ \
            --mpl \
            --mpl-results-path=mpl_results.html \
            --mpl-generate-summary=html \
            --splits 4 \
            --group ${{ matrix.combos.group }} \
            --splitting-algorithm least_duration \
            --db ./prof.db
      - name: save coverage file and plot comparison results
        if: always()
        uses: actions/upload-artifact@v4
        with:
          name: unit_test_artifact-${{ matrix.combos.python_version }}-${{ matrix.combos.group }}
          path: |
            ./cov.xml
            ./mpl_results.html
            ./prof.db
      - name: Upload coverage
        id : codecov
        uses: Wandalen/wretry.action@v1.3.0
        with:
          action: codecov/codecov-action@v3
          with: |
            token: ${{ secrets.CODECOV_TOKEN }}
            name: codecov-umbrella
            files: ./cov.xml
            fail_ci_if_error: true
            verbose: true
          attempt_limit: 10
          attempt_delay: 60000 # ms, 1 min


Linting

name: Linting

on: [push, pull_request]

jobs:
  build:
    runs-on: ubuntu-latest
    strategy:
      max-parallel: 4
      matrix:
        python-version: [3.8]

    steps:
      - uses: actions/checkout@v2
        with:
          fetch-depth: 0
      - name: Set up Python ${{ matrix.python-version }}
        uses: actions/setup-python@v2
        with:
          python-version: ${{ matrix.python-version }}
      - name: Install dependencies
        run: |
          python -m pip install --upgrade pip
          pip install -r requirements-ci.txt --quiet
      - name: mypy
        run: |
          mypy --namespace-packages --explicit-package-bases pymatgen
      - name: ruff
        run: |
          ruff pymatgen
          ruff format pymatgen


Testing

name: Testing

on: [push, pull_request]

jobs:
  build:

    strategy:
      matrix:
        os: [ubuntu-latest]
        python-version: ['3.9']

    runs-on: ${{ matrix.os }}

    env:
      PMG_MAPI_KEY: ${{ secrets.PMG_MAPI_KEY }}
      MPLBACKEND: "Agg"

    steps:
    - uses: actions/checkout@v3
    - name: Set up Python ${{ matrix.python-version }}
      uses: actions/setup-python@v4
      with:
        python-version: ${{ matrix.python-version }}
        cache: pip
        cache-dependency-path: setup.py
    - name: Install dependencies
      run: |
        python -m pip install --upgrade pip
        pip install --quiet -r requirements.txt -r requirements-ci.txt
        pip install -e .
    - name: pytest
      run: |
        pytest --cov=pymatgen.analysis.diffusion --durations=30 pymatgen
    - name: Upload coverage reports to Codecov
      uses: codecov/codecov-action@v3
      env:
        CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}


---
name: Add new issues to OL Engineering Project

on:
  issues:
    types:
    - opened

jobs:
  add-to-project:
    name: Add issue to project
    runs-on: ubuntu-latest
    steps:
    - uses: actions/add-to-project@9bfe908f2eaa7ba10340b31e314148fcfe6a2458 # v1.0.1
      with:
          # You can target a project in a different organization
          # to the issue
        project-url: https://github.com/orgs/mitodl/projects/21
        github-token: ${{ secrets.GH_PROJECT_AUTOMATION_TOKEN }}


Label sync

name: Label sync
# ℹ https://github.com/WordPress/openverse/blob/main/.github/GITHUB.md#label-sync

on:
  workflow_dispatch:
  schedule:
    - cron: "0 0 * * *" # at 00:00

jobs:
  sync_labels:
    name: Sync labels
    runs-on: ubuntu-latest
    # Prevent running this workflow on forks, it's unnecessary for external contributors
    if: github.repository_owner == 'WordPress'
    steps:
      - name: Checkout repository
        uses: actions/checkout@v4

      - name: Sync labels from monorepo to infra
        uses: actions/github-script@v7
        with:
          github-token: ${{ secrets.ACCESS_TOKEN }}
          script: |
            const { main } = await import('${{ github.workspace }}/automations/js/src/sync_labels.mjs')
            await main(github, core)


Meta file sync

name: Meta file sync
# ℹ https://github.com/WordPress/openverse/blob/main/.github/GITHUB.md#meta-file-sync

on:
  workflow_dispatch:
  schedule:
    - cron: "0 0 * * *"
  push:
    branches:
      - main
    paths:
      - "automations/**"
      - ".github/**"

jobs:
  sync_meta:
    name: Sync meta files
    runs-on: ubuntu-latest
    # Prevent running this workflow on forks, it's unnecessary for external contributors
    if: github.repository_owner == 'WordPress'
    steps:
      - name: Checkout repository
        uses: actions/checkout@v4

      - name: Run file sync
        uses: BetaHuhn/repo-file-sync-action@v1
        with:
          GH_PAT: ${{ secrets.ACCESS_TOKEN }} # Can't use GITHUB_TOKEN to make PRs on other repos
          PR_LABELS: |
            🧰 goal: internal improvement
            🤖 aspect: dx
            🟧 priority: high
            🧱 stack: mgmt
            skip-changelog


PR label check

name: PR label check
# ℹ https://github.com/WordPress/openverse/blob/main/.github/GITHUB.md#pr-label-check

on:
  pull_request:
    types:
      # Default types
      # https://docs.github.com/en/actions/using-workflows/events-that-trigger-workflows#pull_request
      - opened
      - reopened
      - synchronize
      # Label is changed
      - labeled
      - unlabeled

jobs:
  # Extract the current label list by group
  get_label_groups:
    name: Get label groups
    runs-on: ubuntu-latest
    outputs:
      aspect: ${{ steps.get_groups.outputs.aspect }}
      goal: ${{ steps.get_groups.outputs.goal }}
      priority: ${{ steps.get_groups.outputs.priority }}
      stack: ${{ steps.get_groups.outputs.stack }}
    steps:
      - name: Checkout repository
        uses: actions/checkout@v4

      - name: Get label groups
        id: get_groups
        env:
          PYTHONPATH: ${{ github.workspace }}/automations/python
        working-directory: automations/python/workflows
        run: python get_label_groups.py

  # List of aspect labels: https://github.com/WordPress/openverse/labels?q=aspect
  check_aspect_label:
    name: Check aspect label
    runs-on: ubuntu-latest
    needs:
      - get_label_groups
    steps:
      - name: Check aspect label
        uses: docker://agilepathway/pull-request-label-checker:v1.6.32
        with:
          any_of: ${{ needs.get_label_groups.outputs.aspect }}
          repo_token: ${{ secrets.GITHUB_TOKEN }}

  # List of goal labels: https://github.com/WordPress/openverse/labels?q=goal
  check_goal_label:
    name: Check goal label
    runs-on: ubuntu-latest
    needs:
      - get_label_groups
    steps:
      - name: Check goal label
        uses: docker://agilepathway/pull-request-label-checker:v1.6.32
        with:
          one_of: ${{ needs.get_label_groups.outputs.goal }}
          repo_token: ${{ secrets.GITHUB_TOKEN }}

  # List of priority labels: https://github.com/WordPress/openverse/labels?q=priority
  check_priority_label:
    name: Check priority label
    runs-on: ubuntu-latest
    needs:
      - get_label_groups
    steps:
      - name: Check priority label
        uses: docker://agilepathway/pull-request-label-checker:v1.6.32
        with:
          one_of: ${{ needs.get_label_groups.outputs.priority }}
          repo_token: ${{ secrets.GITHUB_TOKEN }}

  # List of stack labels: https://github.com/WordPress/openverse/labels?q=stack
  check_stack_label:
    name: Check stack label
    runs-on: ubuntu-latest
    needs:
      - get_label_groups
    steps:
      - name: Check stack label
        uses: docker://agilepathway/pull-request-label-checker:v1.6.32
        with:
          any_of: ${{ needs.get_label_groups.outputs.stack }}
          repo_token: ${{ secrets.GITHUB_TOKEN }}


Weekly updates

name: Weekly updates
# ℹ https://github.com/WordPress/openverse/blob/main/.github/GITHUB.md#project-automation

on:
  workflow_dispatch:
  schedule:
    - cron: "1 0 * * 1" # just after midnight on Monday

env:
  ACCESS_TOKEN: ${{ secrets.ACCESS_TOKEN }}
  MAKE_USERNAME: ${{ secrets.MAKE_USERNAME }}
  MAKE_PASSWORD: ${{ secrets.MAKE_PASSWORD }} # application password, not login password

jobs:
  make_post:
    name: Create post on Make site
    runs-on: ubuntu-latest
    # Prevent running this workflow on forks, it's unnecessary for external contributors
    if: github.repository_owner == 'WordPress'
    steps:
      - name: Checkout repository
        uses: actions/checkout@v4

      - name: Setup CI env
        uses: ./.github/actions/setup-env
        with:
          setup_python: false
          install_recipe: node-install

      - name: Publish post
        run: just automations/js/report


New PR notification

name: New PR notification
# ℹ https://github.com/WordPress/openverse/blob/main/.github/GITHUB.md#new-pr-notification

on:
  pull_request_target:
    types:
      - opened
      - ready_for_review

jobs:
  send_message:
    name: Send Slack message
    # Prevent running this workflow on forks, it's unnecessary for external contributors
    # Also prevent running this for bot-related PRs
    if: |
      github.actor != 'dependabot[bot]' &&
      !startsWith(github.event.pull_request.title, '🔄') &&
      github.repository_owner == 'WordPress'
    runs-on: ubuntu-latest
    env:
      EVENT_ACTION: ${{ github.event.action }}
      SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }}
      SLACK_WEBHOOK_TYPE: INCOMING_WEBHOOK
    steps:
      - name: Write payload to file
        uses: actions/github-script@v7
        with:
          script: |
            let { writeFileSync } = await import('fs')

            const repoName = `${context.repo.owner}/${context.repo.repo}`
            const { pull_request: pr } = context.payload
            const isNew = process.env.EVENT_ACTION === 'opened'
            const emoji = pr.draft ? ':pr-draft:' : ':pull-request:'
            const adjective = isNew ? pr.draft ? 'New draft ' : 'New ' : ''
            const verb = isNew ? 'opened' : 'marked ready for review'

            const payload = {
              text: `${adjective}PR ${verb} by ${pr.user.login} in ${repoName}: #${pr.number} - ${pr.title}`,
              blocks: [
                {
                  type: 'section',
                  text: {
                    type: 'mrkdwn',
                    text: `${emoji} ${adjective}PR ${verb} by *${pr.user.login}* in \`${repoName}\`:\n<${pr.html_url}|#${pr.number} - ${pr.title}>`
                  }
                }
              ]
            }
            console.log(JSON.stringify(payload))
            writeFileSync('/tmp/pr_ping_payload.json', JSON.stringify(payload))

      - name: Send Slack notification
        uses: slackapi/slack-github-action@v1.26.0
        with:
          payload-file-path: /tmp/pr_ping_payload.json


New discussion notification

name: New discussion notification
# ℹ https://github.com/WordPress/openverse/blob/main/.github/GITHUB.md#new-discussion-notification

on:
  discussion:
    types:
      - created

jobs:
  send_message:
    name: Send Slack message
    runs-on: ubuntu-latest
    env:
      discussion_url: ${{ github.event.discussion.html_url }}
      discussion_number: ${{ github.event.discussion.number }}
      discussion_title: ${{ format('{0}', github.event.discussion.title) }}
      discussion_author: ${{ github.event.discussion.user.login }}
      discussion_repo: ${{ github.event.repository.full_name }}
      SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }}
      SLACK_WEBHOOK_TYPE: INCOMING_WEBHOOK
    steps:
      - name: Send notification for new discussion
        id: slack
        uses: slackapi/slack-github-action@v1.26.0
        with:
          payload: |
            {
             "text": "New discussion opened by ${{ env.discussion_author }} in ${{ env.discussion_repo }}: #${{ env.discussion_number }} - ${{ env.discussion_title }}",
             "blocks": [
               {
                 "type": "section",
                 "text": {
                   "type": "mrkdwn",
                   "text": ":github: New discussion opened by *${{ env.discussion_author }}* in ${{ env.discussion_repo }}:\n<${{ env.discussion_url }}|#${{ env.discussion_number }} - ${{ env.discussion_title }}>"
                 }
               }
             ]
            }


Subscribe to Label

name: "Subscribe to Label"
# link: https://github.com/bytecodealliance/subscribe-to-label-action

on:
  issues:
    types: ["labeled"]

jobs:
  subscribe:
    runs-on: ubuntu-latest
    # Prevent running this workflow on forks, it's unnecessary for external contributors
    if: github.repository_owner == 'WordPress'
    steps:
      - uses: bytecodealliance/subscribe-to-label-action@v1
        with:
          repo-token: "${{ secrets.ACCESS_TOKEN }}"


Generate POT file

name: Generate POT file

on:
  push:
    # The workflow will only run when both filters are satisfied.
    paths:
      - frontend/src/locales/scripts/en.json5
    branches:
      - main

concurrency:
  group: ${{ github.workflow }}
  cancel-in-progress: true

jobs:
  generate-pot:
    name: Generate POT file
    runs-on: ubuntu-latest
    # Prevent running this workflow on forks, it's unnecessary for external contributors
    if: github.repository_owner == 'WordPress'
    steps:
      - name: Checkout repository
        uses: actions/checkout@v4

      - name: Setup CI env
        uses: ./.github/actions/setup-env
        with:
          setup_python: false
          install_recipe: node-install

      - name: Generate PO
        run: just frontend/run i18n:generate-pot

      - name: Copy to root
        run: cp frontend/openverse.pot /tmp/openverse.pot

      - name: Checkout repository at `translations` branch
        uses: actions/checkout@v4
        with:
          ref: translations

      - name: Update POT on `translations` branch
        run: |
          rm openverse.pot
          cp /tmp/openverse.pot openverse.pot
          git config --global user.name 'POT Updater'
          git config --global user.email 'openverse@wordpress.org'
          git commit --no-verify -am 'Update POT file'
          git push --no-verify

      - name: Checkout repository # again, to enable cleaning.
        if: always()
        uses: actions/checkout@v4


CI + CD

name: CI + CD

on:
  pull_request:
  push:
    branches:
      - main
  workflow_dispatch:
    inputs:
      image_tag:
        description: "The tag to assign to the images built in the workflow."
        type: string
        required: false
        default: ""
      # This is useful when dispatching on a branch other than `main`.
      perform_deploy:
        description: "Publish images and deploy staging?"
        type: boolean
        required: false
        default: false

concurrency:
  group: ${{ github.workflow }}-${{ github.ref }}
  # Don't continue building images for a PR if the PR is updated quickly
  # For other workflows, allow them to complete and just block on them. This
  # ensures deployments in particular to happen in series rather than parallel.
  cancel-in-progress: ${{ github.event_name == 'pull_request' }}

jobs:
  ###########
  # Helpers #
  ###########

  get-changes:
    name: Get changes
    runs-on: ubuntu-latest
    permissions:
      pull-requests: read
    outputs:
      changes: ${{ steps.paths-filter.outputs.changes }}
      catalog: ${{ contains(fromJson(steps.paths-filter.outputs.changes), 'catalog') }}
      ingestion_server: ${{ contains(fromJson(steps.paths-filter.outputs.changes), 'ingestion_server') }}
      api: ${{ contains(fromJson(steps.paths-filter.outputs.changes), 'api') }}
      frontend: ${{ contains(fromJson(steps.paths-filter.outputs.changes), 'frontend') }}
      documentation: ${{ contains(fromJson(steps.paths-filter.outputs.changes), 'documentation') }}
      ci_cd: ${{ contains(fromJson(steps.paths-filter.outputs.changes), 'ci_cd') }}
      py_packages: ${{ contains(fromJson(steps.paths-filter.outputs.changes), 'py_packages') }}
      js_packages: ${{ contains(fromJson(steps.paths-filter.outputs.changes), 'js_packages') }}
    steps:
      - name: Checkout repository
        uses: actions/checkout@v4

      - name: Get changes
        id: paths-filter
        uses: ./.github/actions/get-changes

  get-image-tag:
    name: Get image tag
    runs-on: ubuntu-latest
    outputs:
      image_tag: ${{ steps.get-image-tag.outputs.image_tag }}

    steps:
      - name: Get image tag
        id: get-image-tag
        run: |
          if [[ "${{ github.event_name }}" == "workflow_dispatch" && "${{ inputs.image_tag }}" != "" ]]; then
              echo "image_tag=${{ inputs.image_tag }}" | tee "$GITHUB_OUTPUT"
          else
              echo "image_tag=${{ github.sha }}" | tee "$GITHUB_OUTPUT"
          fi

  determine-images:
    name: Determine images to build and publish
    runs-on: ubuntu-latest
    outputs:
      do_build: ${{ steps.set-matrix.outputs.do_build }}
      build_matrix: ${{ steps.set-matrix.outputs.build_matrix }}
      do_publish: ${{ steps.set-matrix.outputs.do_publish }}
      publish_matrix: ${{ steps.set-matrix.outputs.publish_matrix }}
    needs:
      - get-changes

    steps:
      - name: Checkout repository
        uses: actions/checkout@v4

      - name: Set matrix images
        id: set-matrix
        env:
          CHANGES: ${{ needs.get-changes.outputs.changes }}
          PYTHONPATH: ${{ github.workspace }}/automations/python
        working-directory: automations/python/workflows
        run: python set_matrix_images.py

  #############
  # Universal #
  #############

  lint: # This includes type-checking of the frontend.
    name: Lint files
    runs-on: ubuntu-latest

    steps:
      - name: Checkout repository
        uses: actions/checkout@v4

      - name: Setup CI env
        uses: ./.github/actions/setup-env
        with:
          # No Python setup is needed, the built-in Python runtime is okay.
          setup_python: false
          # Node.js is needed by lint actions.
          install_recipe: "node-install"

      - name: Cache pre-commit envs
        uses: actions/cache@v4
        with:
          path: ~/.cache/pre-commit
          key: ${{ runner.os }}-pre-commit-${{ hashFiles('.pre-commit-config.yaml') }}

      - name: Run pre-commit to lint files
        env:
          GITHUB_TOKEN: ${{ github.token }}
        run: |
          just precommit
          just lint

  validate-codeowners:
    name: Validate CODEOWNERS
    runs-on: ubuntu-latest

    steps:
      - name: Checkout repository
        uses: actions/checkout@v4

      - uses: mszostok/codeowners-validator@v0.7.4
        with:
          checks: "files,duppatterns,syntax"
          experimental_checks: "notowned,avoid-shadowing"

  build-images:
    name: Build Docker images
    if: needs.determine-images.outputs.do_build == 'true'
    runs-on: ubuntu-latest
    strategy:
      fail-fast: false
      matrix: ${{ fromJson(needs.determine-images.outputs.build_matrix) }}
    needs:
      - get-image-tag
      - lint
      - determine-images

    steps:
      - name: Checkout repository
        uses: actions/checkout@v4

      # Sets up `just` and Node.js.
      # Node.js is needed for the frontend to download translations.
      # `just` is needed to run the recipes and set build args.
      - name: Setup CI env
        uses: ./.github/actions/setup-env
        with:
          setup_python: false
          setup_nodejs: ${{ matrix.image == 'frontend' && 'true' || 'false' }}
          install_recipe: ${{ matrix.image == 'frontend' && 'node-install' || '' }}

      # Sets build args specifying versions needed to build Docker image.
      - name: Prepare build args
        id: prepare-build-args
        run: |
          just versions | tee "$GITHUB_OUTPUT"

      # ℹStep only applies for frontend image.
      # This step
      # - downloads translation strings from GlotPress so that they can be
      #   bundled inside the Docker image
      - name: Prepare frontend for building
        if: matrix.image == 'frontend'
        run: |
          just frontend/run i18n

      - name: Setup Docker Buildx
        uses: docker/setup-buildx-action@v3
        with:
          install: true

      - name: Build image `${{ matrix.image }}`
        uses: docker/build-push-action@v5
        with:
          context: ${{ matrix.context }}
          target: ${{ matrix.target }}
          push: false
          tags: openverse-${{ matrix.image }}
          file: ${{ matrix.file }}
          cache-from: type=gha,scope=${{ matrix.image }}
          cache-to: type=gha,scope=${{ matrix.image }}
          outputs: type=docker,dest=/tmp/${{ matrix.image }}.tar
          build-contexts: ${{ matrix.build-contexts }}
          build-args: |
            SEMANTIC_VERSION=${{ needs.get-image-tag.outputs.image_tag }}
            CATALOG_PY_VERSION=${{ steps.prepare-build-args.outputs.catalog_py_version }}
            CATALOG_AIRFLOW_VERSION=${{ steps.prepare-build-args.outputs.catalog_airflow_version }}
            API_PY_VERSION=${{ steps.prepare-build-args.outputs.api_py_version }}
            INGESTION_PY_VERSION=${{ steps.prepare-build-args.outputs.ingestion_py_version }}
            FRONTEND_NODE_VERSION=${{ steps.prepare-build-args.outputs.frontend_node_version }}
            FRONTEND_PNPM_VERSION=${{ steps.prepare-build-args.outputs.frontend_pnpm_version }}
            PGCLI_VERSION=${{ steps.prepare-build-args.outputs.pgcli_version }}
            ${{ matrix.build-args || '' }}

      - name: Upload image `${{ matrix.image }}`
        id: upload-img
        uses: actions/upload-artifact@v4
        with:
          name: ${{ matrix.image }}
          path: /tmp/${{ matrix.image }}.tar

      - name: Show artifact ID
        run: |
          echo '${{ matrix.image }} artifact ID is ${{ steps.upload-img.outputs.artifact-id }}'

  ###########
  # Catalog #
  ###########

  test-cat:
    name: Run tests for the catalog
    if: |
      needs.get-changes.outputs.catalog == 'true' ||
      needs.get-changes.outputs.ci_cd == 'true'
    runs-on: ubuntu-latest
    timeout-minutes: 15
    needs:
      - get-changes
      - build-images

    steps:
      - name: Checkout repository
        uses: actions/checkout@v4

      - name: Setup CI env
        uses: ./.github/actions/setup-env
        with:
          setup_python: false
          # Python is not needed to run the tests.
          setup_nodejs: false
          # Node.js is not needed to run the tests.
          install_recipe: ""

      - name: Load Docker images
        uses: ./.github/actions/load-img
        with:
          run_id: ${{ github.run_id }}
          setup_images: upstream_db catalog

      - name: Run tests
        run: |
          just catalog/test --extended

  catalog-checks:
    name: Run catalog checks
    if: |
      needs.get-changes.outputs.catalog == 'true' ||
      needs.get-changes.outputs.ci_cd == 'true'
    runs-on: ubuntu-latest
    needs:
      - get-changes
      - build-images

    steps:
      - name: Checkout repository
        uses: actions/checkout@v4

      - name: Setup CI env
        uses: ./.github/actions/setup-env
        with:
          # Python is not needed to run the tests.
          setup_python: false
          # Node.js is needed by lint actions.
          install_recipe: "node-install"

      - name: Load Docker images
        uses: ./.github/actions/load-img
        with:
          run_id: ${{ github.run_id }}
          setup_images: upstream_db catalog

      - name: Check generated documentation
        run: |
          just catalog/generate-docs dag true
          just catalog/generate-docs media-props true

  ####################
  # Ingestion server #
  ####################

  test-ing:
    name: Run tests for ingestion-server
    if: |
      needs.get-changes.outputs.ingestion_server == 'true' ||
      needs.get-changes.outputs.ci_cd == 'true'
    runs-on: ubuntu-latest
    timeout-minutes: 15
    needs:
      - get-changes
      - build-images

    steps:
      - name: Checkout repository
        uses: actions/checkout@v4

      - name: Setup CI env
        uses: ./.github/actions/setup-env
        with:
          # Python is needed to run the test.
          setup_nodejs: false
          # Node.js is not needed to run ingestion server tests.
          install_recipe: ingestion_server/install

      - name: Load Docker images
        uses: ./.github/actions/load-img
        with:
          run_id: ${{ github.run_id }}
          setup_images: upstream_db ingestion_server

      - name: Run ingestion-server tests
        run: |
          just env
          just ingestion_server/test-local

      - name: Print ingestion-server test logs
        run: |
          just ingestion_server/test-logs > ingestion_server/test/ingestion_logs.txt
          cat ingestion_server/test/ingestion_logs.txt

      - name: Upload ingestion test logs
        if: success() || failure()
        uses: actions/upload-artifact@v4
        with:
          name: ing_logs
          path: ingestion_server/test/ingestion_logs.txt

  #######
  # API #
  #######

  test-api:
    name: Run tests for the API
    if: |
      needs.get-changes.outputs.ingestion_server == 'true' ||
      needs.get-changes.outputs.api == 'true' ||
      needs.get-changes.outputs.ci_cd == 'true'
    runs-on: ubuntu-latest
    timeout-minutes: 15
    needs:
      - get-changes
      - build-images
      - get-image-tag

    steps:
      - name: Checkout repository
        uses: actions/checkout@v4

      - name: Setup CI env
        uses: ./.github/actions/setup-env
        with:
          setup_python: false
          # Python is not needed to run the tests.
          setup_nodejs: false
          # Node.js is not needed to run API tests.
          install_recipe: ""

      - name: Load Docker images
        uses: ./.github/actions/load-img
        with:
          run_id: ${{ github.run_id }}
          setup_images: upstream_db ingestion_server

      # Sets build args specifying versions needed to build Docker image.
      - name: Prepare build args
        id: prepare-build-args
        run: |
          just versions | tee "$GITHUB_OUTPUT"

      - name: Setup Docker Buildx
        uses: docker/setup-buildx-action@v3
        with:
          install: true

      - name: Build API dev image
        uses: docker/build-push-action@v5
        with:
          context: api
          target: api
          push: false
          load: true
          tags: openverse-api
          cache-from: type=gha,scope=api_dev
          cache-to: type=gha,scope=api_dev
          build-contexts: packages=./packages/python
          build-args: |
            SEMANTIC_VERSION=${{ needs.get-image-tag.outputs.image_tag }}
            API_PY_VERSION=${{ steps.prepare-build-args.outputs.api_py_version }}
            PDM_INSTALL_ARGS=--dev

      - name: Start API, ingest and index test data
        run: just api/init

      - name: Run API tests
        run: just api/test

      - name: Print API test logs
        if: success() || failure()
        run: |
          just logs > api_logs
          cat api_logs

      - name: Upload API test logs
        if: success() || failure()
        uses: actions/upload-artifact@v4
        with:
          name: api_logs
          path: api_logs

  django-checks:
    name: Run Django checks
    if: |
      needs.get-changes.outputs.api == 'true' ||
      needs.get-changes.outputs.ci_cd == 'true'
    runs-on: ubuntu-latest
    needs:
      - get-changes
      - build-images
    strategy:
      fail-fast: false
      matrix:
        name:
          - check_django
          - validate_openapi
          - check_migrations
          - test_doc
          - test_media_props
        include:
          - name: check_django
            recipe: api/dj check
          - name: validate_openapi
            recipe: api/dj spectacular --format openapi-json --validate --file openapi.json
          - name: check_migrations
            recipe: api/dj makemigrations --check
          - name: test_doc
            recipe: api/doc-test
          - name: test_media_props
            recipe: api/generate-docs

    steps:
      - name: Checkout repository
        uses: actions/checkout@v4

      - name: Setup CI env
        uses: ./.github/actions/setup-env
        with:
          setup_python: false
          # Python is not needed to run the tests.
          setup_nodejs: false
          # Node.js is not needed to run API tests.
          install_recipe: ""

      - name: Load Docker images
        uses: ./.github/actions/load-img
        with:
          run_id: ${{ github.run_id }}
          setup_images: upstream_db ingestion_server api

      - name: Run check recipe
        run: just ${{ matrix.recipe }}
        env:
          DC_USER: root

      - name: Upload schema
        if: matrix.name == 'test_doc'
        uses: actions/upload-artifact@v4
        with:
          name: openapi.json
          path: ./api/openapi.json

  # This job runs when `django-checks` doesn't and always passes, thus allowing
  # PRs to meet the required checks criteria and be merged.
  bypass-django-checks:
    name: Run Django checks
    if: |
      !cancelled() &&
      needs.django-checks.result == 'skipped'
    runs-on: ubuntu-latest
    needs:
      - django-checks
    strategy:
      matrix:
        name:
          - check_django
          - validate_openapi
          - check_migrations
          - test_doc

    steps:
      - name: Pass
        run: echo 'Django checks are skipped because API is unchanged.'

  py-package-checks:
    name: Run checks for Python packages/*
    if: |
      needs.get-changes.outputs.py_packages == 'true'
    runs-on: ubuntu-latest
    needs:
      - get-changes
      - lint
    strategy:
      fail-fast: false
      matrix:
        packages:
          - packages/python/openverse-attribution
        script:
          - build
          - test

    steps:
      - name: Checkout repository
        uses: actions/checkout@v4

      - name: Setup CI env
        uses: ./.github/actions/setup-env
        with:
          install_recipe: packages/python/openverse-attribution/install

      - name: Run Python packages checks
        run: pdm run -p ${{ matrix.packages }} ${{ matrix.script }}

  # This job runs when `py-package-checks` doesn't and always passes, thus
  # allowing PRs to meet the required checks criteria and be merged.
  bypass-py-package-checks:
    name: Run checks for Python packages/*
    if: |
      !cancelled() &&
      needs.py-package-checks.result == 'skipped'
    runs-on: ubuntu-latest
    needs:
      - py-package-checks
    strategy:
      fail-fast: false
      matrix:
        name:
          - build
          - unit_test

    steps:
      - name: Pass
        run: echo 'Checks for Python packages are skipped because they are unchanged.'

  ############
  # Frontend #
  ############

  nuxt-build:
    name: Check Nuxt build
    if: |
      needs.get-changes.outputs.frontend == 'true' ||
      needs.get-changes.outputs.ci_cd == 'true'
    runs-on: ubuntu-latest
    needs:
      - get-changes
      - lint

    steps:
      - name: Checkout repository
        uses: actions/checkout@v4

      - name: Setup CI env
        uses: ./.github/actions/setup-env
        with:
          setup_python: false
          install_recipe: node-install

      - name: Run build
        run: just frontend/run build

  nuxt-checks:
    name: Run Nuxt checks
    if: |
      needs.get-changes.outputs.frontend == 'true' ||
      needs.get-changes.outputs.ci_cd == 'true'
    runs-on: ubuntu-latest
    needs:
      - get-changes
      - lint
    strategy:
      fail-fast: false
      matrix:
        name:
          - unit_test
          - test_media_props
          - frontend_init
        include:
          - name: unit_test
            recipe: frontend/run test:unit
          - name: test_media_props
            recipe: frontend/generate-docs
          - name: frontend_init
            recipe: frontend/init

    steps:
      - name: Checkout repository
        uses: actions/checkout@v4

      - name: Setup CI env
        uses: ./.github/actions/setup-env
        with:
          setup_python: false
          install_recipe: node-install

      - name: Run check recipe
        run: just ${{ matrix.recipe }}

  # This job runs when `nuxt-checks` doesn't and always passes, thus allowing
  # PRs to meet the required checks criteria and be merged.
  bypass-nuxt-checks:
    name: Run Nuxt checks
    if: |
      !cancelled() &&
      needs.nuxt-checks.result == 'skipped'
    runs-on: ubuntu-latest
    needs:
      - nuxt-checks
    strategy:
      fail-fast: false
      matrix:
        name:
          - unit_test

    steps:
      - name: Pass
        run: echo 'Playwright tests are skipped because frontend is unchanged.'

  js-package-checks:
    name: Run checks for JS packages/*
    if: |
      needs.get-changes.outputs.js_packages == 'true'
    runs-on: ubuntu-latest
    needs:
      - get-changes
      - lint
    strategy:
      fail-fast: false
      matrix:
        name:
          - build
          - unit_test
        include:
          - name: build
            script: "build"
          - name: unit_test
            script: "test:unit"

    steps:
      - name: Checkout repository
        uses: actions/checkout@v4

      - name: Setup CI env
        uses: ./.github/actions/setup-env
        with:
          setup_python: false
          install_recipe: node-install

      - name: Run JS packages checks
        run: pnpm --filter ./packages/js/* run --aggregate-output ${{ matrix.script }}

  # This job runs when `js-package-checks` doesn't and always passes, thus
  # allowing PRs to meet the required checks criteria and be merged.
  bypass-js-package-checks:
    name: Run checks for JS packages/*
    if: |
      !cancelled() &&
      needs.js-package-checks.result == 'skipped'
    runs-on: ubuntu-latest
    needs:
      - js-package-checks
    strategy:
      fail-fast: false
      matrix:
        name:
          - build
          - unit_test

    steps:
      - name: Pass
        run: echo 'Checks for JS packages are skipped because they are unchanged.'

  playwright:
    name: Run Playwright tests
    if: |
      needs.get-changes.outputs.frontend == 'true' ||
      needs.get-changes.outputs.ci_cd == 'true'
    runs-on: ubuntu-latest
    timeout-minutes: 15
    needs:
      - get-changes
      - lint
    strategy:
      fail-fast: false
      matrix:
        name:
          - playwright_vr
          - playwright_e2e
          - storybook
        include:
          - name: playwright_vr
            script: "test:playwright visual-regression"
          - name: playwright_e2e
            script: "test:playwright e2e"
          - name: storybook
            script: "test:storybook"

    steps:
      - name: Checkout repository
        uses: actions/checkout@v4

      - name: Setup CI env
        uses: ./.github/actions/setup-env
        with:
          setup_python: false
          install_recipe: node-install

      - name: Run Playwright tests
        run: just frontend/run ${{ matrix.script }} --workers=2

      - uses: actions/upload-artifact@v4
        if: failure()
        id: test-results
        with:
          name: ${{ matrix.name }}_test_results
          path: frontend/test-results

  # This job runs when `playwright` doesn't and always passes, thus allowing
  # PRs to meet the required checks criteria and be merged.
  bypass-playwright:
    name: Run Playwright tests
    if: |
      !cancelled() &&
      needs.playwright.result == 'skipped'
    runs-on: ubuntu-latest
    needs:
      - playwright
    strategy:
      matrix:
        name:
          - playwright_vr
          - playwright_e2e
          - storybook

    steps:
      - name: Pass
        run: echo 'Playwright tests are skipped because frontend is unchanged.'

  playwright-test-failure-comment:
    name: Post Playwright test debugging instructions
    if: |
      !cancelled() &&
      github.event_name == 'pull_request' &&
      needs.playwright.result != ''
    runs-on: ubuntu-latest
    needs:
      - playwright

    steps:
      - uses: peter-evans/find-comment@v3
        id: test-results-comment
        with:
          issue-number: ${{ github.event.pull_request.number }}
          body-includes: Playwright failure test results

      - name: Delete existing results comment
        uses: actions/github-script@v7
        if: steps.test-results-comment.outputs.comment-id != 0
        with:
          script: |
            await github.rest.issues.deleteComment({
              repo: context.repo.repo,
              owner: context.repo.owner,
              comment_id: ${{ steps.test-results-comment.outputs.comment-id }}
            })
            console.log('Deleted comment with ID ${{ steps.test-results-comment.outputs.comment-id }}')

      - name: Build help body
        if: needs.playwright.result == 'failure'
        id: help-body
        run: |
          EOF=$(dd if=/dev/urandom bs=15 count=1 status=none | base64) # Security hardening: https://docs.github.com/en/actions/security-guides/security-hardening-for-github-actions#understanding-the-risk-of-script-injections
          MESSAGE=$(cat <<HEREDOC
          help_body<<$EOF
          **Playwright failure test results**: <https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}>

          It looks like some of the Playwright tests failed. You can download the trace output and image diffs for the failed tests under the "Artifacts" section in the above page.

          You can read the [docs on how to use this artifact](https://docs.openverse.org/frontend/reference/playwright_tests.html#debugging).

          If the test is flaky, follow the [flaky test triage procedure](https://docs.openverse.org/general/test.html#flaky-tests).
          $EOF
          HEREDOC
          )
          echo "$MESSAGE" | tee "$GITHUB_OUTPUT"

      - uses: peter-evans/create-or-update-comment@v4
        id: create-comment
        # Do not leave a comment on forks
        if: |
          needs.playwright.result == 'failure' &&
          (
            github.event_name == 'pull_request' &&
            github.event.pull_request.head.repo.owner.login == 'WordPress' &&
            github.actor != 'dependabot[bot]'
          )
        with:
          issue-number: ${{ github.event.pull_request.number }}
          body: ${{ steps.help-body.outputs.help_body }}

  #################
  # Documentation #
  #################

  build-docs:
    name: Build full-stack docs
    if: |
      needs.get-changes.outputs.documentation == 'true' ||
      needs.get-changes.outputs.ci_cd == 'true'
    runs-on: ubuntu-latest
    needs:
      - get-changes
      - lint

    steps:
      - name: Checkout repository
        uses: actions/checkout@v4

      - name: Setup CI env
        uses: ./.github/actions/setup-env

      - name: Compile documentation
        uses: ./.github/actions/build-docs
        with:
          github_token: ${{ secrets.GITHUB_TOKEN }}
        # Docs will be located at `/tmp/docs`.

      - name: Upload documentation
        uses: actions/upload-artifact@v4
        with:
          name: documentation
          path: /tmp/docs/

  emit-docs:
    name: Emit full-stack docs
    # https://github.com/actions/runner/issues/491#issuecomment-850884422
    if: |
      !failure() && !cancelled() &&
      (
        (
          github.event_name == 'push' &&
          github.repository == 'WordPress/openverse'
        ) ||
        (
          github.event_name == 'pull_request' &&
          github.event.pull_request.head.repo.owner.login == 'WordPress' &&
          github.actor != 'dependabot[bot]'
        )
      ) &&
      needs.get-changes.outputs.documentation == 'true' &&
      (needs.test-ing.result == 'success' || needs.test-ing.result == 'skipped') &&
      (needs.test-api.result == 'success' || needs.test-api.result == 'skipped') &&
      (needs.test-cat.result == 'success' || needs.test-cat.result == 'skipped') &&
      (needs.playwright.result == 'success' || needs.playwright.result == 'skipped') &&
      needs.build-docs.result == 'success'
    runs-on: ubuntu-latest
    needs:
      - get-changes
      - test-cat
      - test-ing
      - test-api
      - playwright
      - build-docs

    steps:
      - name: Download documentation
        uses: actions/download-artifact@v4
        with:
          name: documentation
          path: /tmp/docs

      - name: Recreate working directory # to avoid superfluous files from getting tracked automatically
        run: |
          cd ..
          sudo rm -rf openverse
          mkdir openverse

      - name: Checkout repository at `gh-pages` branch
        uses: actions/checkout@v4
        with:
          ref: gh-pages
          path: gh-pages

      - name: Checkout automations from repository
        uses: actions/checkout@v4
        with:
          path: automation-checkout

      - name: Copy existing previews
        if: github.event_name == 'push'
        run: |
          mv /tmp/docs /tmp/gh-pages
          mv gh-pages/_preview /tmp/gh-pages/_preview

      - name: Replace preview of current PR
        if: github.event_name == 'pull_request'
        run: |
          cp -r gh-pages /tmp/gh-pages
          sudo rm -rf /tmp/gh-pages/_preview/${{ github.event.pull_request.number }}
          mv /tmp/docs /tmp/gh-pages/_preview/${{ github.event.pull_request.number }}

      - name: Determine which files have changed
        if: github.event_name == 'pull_request'
        id: preview_diff
        env:
          PR_NUMBER: ${{ github.event.pull_request.number }}
          PYTHONPATH: ${{ github.workspace }}/automation-checkout/automations/python
        working-directory: ${{ github.workspace }}/automation-checkout/automations/python/workflows
        run: python get_folder_differences.py

      - name: Deploy
        uses: peaceiris/actions-gh-pages@v3
        with:
          github_token: ${{ secrets.GITHUB_TOKEN }}
          publish_dir: /tmp/gh-pages
          force_orphan: true
          cname: docs.openverse.org

      - uses: peter-evans/find-comment@v3
        if: github.event_name == 'pull_request'
        id: final-preview-comment
        with:
          issue-number: ${{ github.event.pull_request.number }}
          body-includes: Full-stack documentation

      - uses: peter-evans/create-or-update-comment@v4
        if: github.event_name == 'pull_request'
        with:
          issue-number: ${{ github.event.pull_request.number }}
          comment-id: ${{ steps.final-preview-comment.outputs.comment-id }}
          edit-mode: replace
          body: ${{ steps.preview_diff.outputs.body }}

      - name: Checkout repository # again, to enable cleaning
        uses: actions/checkout@v4
        if: success() || failure()

  #################
  # Docker images #
  #################

  publish-images:
    name: Publish Docker images
    runs-on: ubuntu-latest
    # prevent running on fork PRs
    if: |
      !failure() && !cancelled() &&
      (
        (github.event_name == 'push' && github.repository == 'WordPress/openverse') ||
        (github.event_name == 'workflow_dispatch' && inputs.perform_deploy)
      ) &&
      needs.determine-images.outputs.do_publish == 'true' &&
      (needs.test-ing.result == 'success' || needs.test-ing.result == 'skipped') &&
      (needs.test-api.result == 'success' || needs.test-api.result == 'skipped') &&
      (needs.test-cat.result == 'success' || needs.test-cat.result == 'skipped') &&
      (needs.playwright.result == 'success' || needs.playwright.result == 'skipped')
    needs:
      - determine-images
      - get-image-tag
      - build-images
      - test-ing # test for ingestion server
      - test-api # test for API
      - test-cat # test for catalog
      - playwright # test for frontend
    permissions:
      packages: write
      contents: read
    strategy:
      fail-fast: false
      matrix: ${{ fromJson(needs.determine-images.outputs.publish_matrix) }}

    steps:
      - name: Checkout repository
        uses: actions/checkout@v4

      - name: Log in to GitHub Docker Registry
        uses: docker/login-action@v3
        with:
          registry: https://ghcr.io
          username: ${{ github.actor }}
          password: ${{ secrets.GITHUB_TOKEN }}

      - name: Load Docker image `${{ matrix.image }}`
        uses: ./.github/actions/load-img
        with:
          run_id: ${{ github.run_id }}
          setup_images: ${{ matrix.image }}

      - name: Load and tag image `${{ matrix.image }}` (latest & sha)
        run: |
          docker tag openverse-${{ matrix.image }} \
            ghcr.io/wordpress/openverse-${{ matrix.image }}:latest
          docker tag openverse-${{ matrix.image }} \
            ghcr.io/wordpress/openverse-${{ matrix.image }}:${{ needs.get-image-tag.outputs.image_tag }}
          docker push --all-tags ghcr.io/wordpress/openverse-${{ matrix.image }}

  ##############
  # Deployment #
  ##############

  # See https://github.com/WordPress/openverse/issues/1033 for why
  # we don't use the standard reusable workflow approach for these.

  deploy-frontend:
    name: Deploy staging frontend
    runs-on: ubuntu-latest
    if: |
      !failure() && !cancelled() &&
      (
        (github.event_name == 'push' && github.repository == 'WordPress/openverse') ||
        (github.event_name == 'workflow_dispatch' && inputs.perform_deploy)
      ) &&
      needs.get-changes.outputs.frontend == 'true' &&
      needs.playwright.result == 'success' &&
      needs.publish-images.result == 'success'
    needs:
      - get-changes
      - playwright
      - get-image-tag
      - publish-images
    steps:
      - name: Checkout repository
        uses: actions/checkout@v4

      - name: Get commit message
        id: commit
        env:
          COMMIT_MESSAGE: ${{ github.event.head_commit.message }}
          PYTHONPATH: ${{ github.workspace }}/automations/python
        working-directory: automations/python/workflows
        run: python get_commit_message.py

      - name: Deploy staging frontend
        uses: felixp8/dispatch-and-wait@v0.1.0
        with:
          owner: WordPress
          repo: openverse-infrastructure
          token: ${{ secrets.ACCESS_TOKEN }}
          event_type: deploy_staging_nuxt
          client_payload: |
            {
              "actor": "${{ github.actor }}",
              "tag": "${{ needs.get-image-tag.outputs.image_tag }}",
              "run_name": "${{ steps.commit.outputs.commit_message }}"
            }
          wait_time: 60 # check every minute
          max_time: 1800 # allow up to 30 minutes for a deployment

  deploy-api:
    name: Deploy staging API
    runs-on: ubuntu-latest
    if: |
      !failure() && !cancelled() &&
      (
        (github.event_name == 'push' && github.repository == 'WordPress/openverse') ||
        (github.event_name == 'workflow_dispatch' && inputs.perform_deploy)
      ) &&
      needs.get-changes.outputs.api == 'true' &&
      needs.publish-images.result == 'success'
    needs:
      - get-changes
      - get-image-tag
      - publish-images
    steps:
      - name: Checkout repository
        uses: actions/checkout@v4

      - name: Get commit message
        id: commit
        env:
          COMMIT_MESSAGE: ${{ github.event.head_commit.message }}
          PYTHONPATH: ${{ github.workspace }}/automations/python
        working-directory: automations/python/workflows
        run: python get_commit_message.py

      - name: Deploy staging API
        uses: felixp8/dispatch-and-wait@v0.1.0
        with:
          owner: WordPress
          repo: openverse-infrastructure
          token: ${{ secrets.ACCESS_TOKEN }}
          event_type: deploy_staging_api
          client_payload: |
            {
              "actor": "${{ github.actor }}",
              "tag": "${{ needs.get-image-tag.outputs.image_tag }}",
              "run_name": "${{ steps.commit.outputs.commit_message }}"
            }
          wait_time: 60 # check every minute
          max_time: 1800 # allow up to 30 minutes for a deployment

  ################
  # Notification #
  ################

  send-report:
    name: Send Slack report
    runs-on: ubuntu-latest
    if: |
      !cancelled() &&
      (
        (github.event_name == 'push' && github.repository == 'WordPress/openverse') ||
        (github.event_name == 'workflow_dispatch' && inputs.perform_deploy)
      ) &&
      (
        ((github.event_name == 'push' && needs.get-changes.outputs.documentation == 'true') && needs.emit-docs.result != 'success') ||
        (needs.determine-images.outputs.do_publish == 'true' && needs.publish-images.result != 'success') ||
        (needs.get-changes.outputs.frontend == 'true' && needs.deploy-frontend.result != 'success') ||
        (needs.get-changes.outputs.api == 'true' && needs.deploy-api.result != 'success')
      )
    needs: # the end products of the CI + CD workflow
      - get-changes
      - determine-images
      - emit-docs
      - publish-images
      - deploy-frontend
      - deploy-api

    steps:
      - name: Checkout repository
        uses: actions/checkout@v4

      - name: Get commit message
        id: commit
        env:
          COMMIT_MESSAGE: ${{ github.event.head_commit.message }}
          PYTHONPATH: ${{ github.workspace }}/automations/python
        working-directory: automations/python/workflows
        run: python get_commit_message.py

      - name: Generate report
        id: report
        env:
          COMMIT_MESSAGE: ${{ steps.commit.outputs.commit_message }}
          DEPLOY_API_RESULT: ${{ needs.deploy-api.result }}
          DEPLOY_FRONTEND_RESULT: ${{ needs.deploy-frontend.result }}
          EMIT_DOCS_RESULT: ${{ needs.emit-docs.result }}
          GH_SLACK_USERNAME_MAP: ${{ secrets.GH_SLACK_USERNAME_MAP }}
          GITHUB_ACTOR: ${{ github.event.head_commit.author.username }}
          PUBLISH_IMAGES_RESULT: ${{ needs.publish-images.result }}
          PYTHONPATH: ${{ github.workspace }}/automations/python
          REPOSITORY: ${{ github.repository }}
          RUN_ID: ${{ github.run_id }}
          SERVER_URL: ${{ github.server_url }}
        working-directory: automations/python/workflows
        run: python generate_report.py

      - name: Send report
        uses: slackapi/slack-github-action@v1.26.0
        with:
          payload: ${{ steps.report.outputs.payload }}
        env:
          SLACK_WEBHOOK_URL: ${{ secrets.SLACK_OV_ALERTS_WEBHOOK_URL }}
          SLACK_WEBHOOK_TYPE: INCOMING_WEBHOOK


Migration Safety Check

name: "Migration Safety Check"

on:
  pull_request:
    paths:
      - "**/migrations/**/*"

concurrency:
  # The concurrency group contains the workflow name and the branch name for pull requests.
  group: ${{ github.workflow }}-${{ github.head_ref }}
  cancel-in-progress: true

jobs:
  check-and-apply-labels:
    name: "Check and Apply Labels"
    runs-on: ubuntu-latest

    steps:
      - uses: actions/checkout@v4

      - uses: peter-evans/find-comment@v3
        id: existing-warning
        with:
          issue-number: ${{ github.event.pull_request.number }}
          body-includes: This PR has migrations

      - name: Delete existing migration warning comment
        uses: actions/github-script@v7
        if: steps.existing-warning.outputs.comment-id != 0
        with:
          github-token: ${{ secrets.ACCESS_TOKEN }}
          script: |
            await github.rest.issues.deleteComment({
              owner: 'WordPress',
              repo: 'openverse',
              comment_id: ${{ steps.existing-warning.outputs.comment-id }},
            })

      - uses: peter-evans/create-or-update-comment@v4
        with:
          issue-number: ${{ github.event.pull_request.number }}
          body: |
            This PR has migrations. Please rebase it before merging to ensure that conflicting migrations are not introduced.


PR Closed

name: PR Closed

on:
  pull_request:
    types:
      - closed

jobs:
  clean-gh-pages:
    name: Clean GH Pages preview
    runs-on: ubuntu-latest

    steps:
      - uses: actions/checkout@v4
        with:
          ref: gh-pages

      - name: Determine if cleaning is necessary
        id: should-clean
        run: |
          if [ -d "_preview/${{ github.event.pull_request.number }}" ]; then
            echo "PR preview exists, cleaning"
            echo "clean=true" >> "$GITHUB_OUTPUT"
          else
            echo "PR preview does not exist, skipping"
            echo "clean=false" >> "$GITHUB_OUTPUT"
          fi

      - name: Remove PR preview
        run: rm -rf _preview/${{ github.event.pull_request.number }}

      - name: Push cleaned branch
        if: ${{ steps.should-clean.outputs.clean == 'true' }}
        run: |
          git config --global user.name 'PR Closed action'
          git config --global user.email 'openverse@wordpress.org'
          git commit -am 'Clean preview for PR ${{ github.event.pull_request.number }}'
          git push


Renovate

name: Renovate
on:
  # This lets you dispatch a renovate job with different cache options if you want to reset or disable the cache manually.
  workflow_dispatch:
    inputs:
      repoCache:
        description: "Reset or disable the cache?"
        type: choice
        default: enabled
        options:
          - enabled
          - disabled
          - reset
  schedule:
    # Run every 30 minutes:
    - cron: "0,30 * * * *"

# Adding these as env variables makes it easy to reuse them in different steps and in bash.
env:
  cache_archive: renovate_cache.tar.gz
  # This is the dir renovate provides -- if we set our own directory via cacheDir, we can run into permissions issues.
  # It is also possible to cache a higher level of the directory, but it has minimal benefit. While renovate execution
  # time gets faster, it also takes longer to upload the cache as it grows bigger.
  cache_dir: /tmp/renovate/cache/renovate/repository
  # This can be manually changed to bust the cache if necessary.
  cache_key: renovate-cache

jobs:
  renovate:
    runs-on: ubuntu-latest
    # Prevent running this workflow on forks, it's unnecessary for external contributors
    if: github.repository_owner == 'WordPress'
    steps:
      - uses: actions/checkout@v4

      - uses: actions/cache@v4
        if: github.event.inputs.repoCache != 'disabled'
        continue-on-error: true
        with:
          path: cache-download
          key: ${{ env.cache_key }}

      # Using tar to compress and extract the archive isn't strictly necessary, but it can improve
      # performance significantly when uploading artifacts with lots of files.
      - name: Extract renovate cache
        run: |
          set -x
          # Skip if no cache is set, such as the first time it runs.
          if [ ! -d cache-download ] ; then
            echo "No cache found."
            exit 0
          fi

          # Make sure the directory exists, and extract it there. Note that it's nested in the download directory.
          mkdir -p "$cache_dir"
          tar -xzf "cache-download/$cache_archive" -C "$cache_dir"

          # Unfortunately, the permissions expected within renovate's docker container
          # are different than the ones given after the cache is restored. We have to
          # change ownership to solve this. We also need to have correct permissions in
          # the entire /tmp/renovate tree, not just the section with the repo cache.
          sudo chown -R runneradmin:root /tmp/renovate/
          ls -R "$cache_dir"

      - uses: renovatebot/github-action@v40.1.11
        with:
          configurationFile: .github/renovate.json
          token: ${{ secrets.ACCESS_TOKEN }}
        env:
          # This enables the cache -- if this is set, it's not necessary to add it to renovate.json.
          RENOVATE_REPOSITORY_CACHE: ${{ github.event.inputs.repoCache || 'enabled' }}
          # Debug logs to figure out why vulnerability updates aren't occurring
          LOG_LEVEL: debug

      # Compression helps performance in the upload step!
      - name: Compress renovate cache
        run: |
          ls "$cache_dir"
          # The -C is important -- otherwise we end up extracting the files with
          # their full path, ultimately leading to a nested directory situation.
          # To solve *that*, we'd have to extract to root (/), which isn't safe.
          tar -czvf "$cache_archive" -C "$cache_dir" .

      - uses: actions/upload-artifact@v4
        if: github.event.inputs.repoCache != 'disabled'
        with:
          name: ${{ env.cache_key }}
          path: ${{ env.cache_archive }}
          # Since this is updated and restored on every run, we don't need to keep it
          # for long. Just make sure this value is large enough that multiple renovate
          # runs can happen before older cache archives are deleted.
          retention-days: 1


Release app

name: Release app

on:
  release:
    types: [released]

# Prevent concurrency with other workflows in the "Release workflow group"
# See https://github.com/WordPress/openverse/issues/4505 for context
concurrency:
  group: operates_on_github_releases
  cancel-in-progress: false

jobs:
  release-app:
    name: Release app
    runs-on: ubuntu-latest
    # Prevent running this workflow on forks, it's unnecessary for external contributors
    if: github.repository_owner == 'WordPress'
    permissions:
      # Needed to create the GitHub release on the repo
      contents: write
      # Needed for pushing the new docker image tag
      packages: write
      # Needed to open the changelog PR
      pull-requests: write
    env:
      # Do not split this into multiple lines, it will not work:
      # https://github.com/WordPress/openverse/pull/3789#pullrequestreview-1876525552
      APP_NAME: ${{ startsWith(github.ref_name, 'api-') && 'api' || startsWith(github.ref_name, 'ingestion_server-') && 'ingestion_server' || startsWith(github.ref_name, 'catalog-') && 'catalog' || startsWith(github.ref_name, 'frontend-') && 'frontend' }}
    steps:
      - uses: actions/checkout@v4
        with:
          # Creating the tag requires having the whole history of `main`
          fetch-depth: 0

      - name: Calculate tag name
        id: tag
        run: |
          release_date=$(echo '${{ github.ref_name }}' | sed 's/${{ env.APP_NAME }}-//')

          # Generate `rel-` prefixed image tag to avoid duplicated app name between image and tag
          {
            echo "date=$release_date";
            echo "image-tag=rel-$release_date";
          } >> "$GITHUB_OUTPUT"

      - name: Log in to GitHub Docker Registry
        uses: docker/login-action@v3
        with:
          registry: https://ghcr.io
          username: ${{ github.repository_owner }}
          password: ${{ secrets.GITHUB_TOKEN }}

      - name: Tag latest with release tag
        run: |
          docker buildx imagetools create ghcr.io/wordpress/openverse-${{ env.APP_NAME }}:latest --tag ghcr.io/wordpress/openverse-${{ env.APP_NAME }}:${{ steps.tag.outputs.image-tag }}

          if [[ "${{ env.APP_NAME }}" == "api" ]] || [[ "${{ env.APP_NAME }}" == "frontend" ]]; then
            docker buildx imagetools create ghcr.io/wordpress/openverse-${{ env.APP_NAME }}_nginx:latest --tag ghcr.io/wordpress/openverse-${{ env.APP_NAME }}_nginx:${{ steps.tag.outputs.image-tag }}
          fi

      - name: Deploy production application
        if: env.APP_NAME == 'frontend' || env.APP_NAME == 'api'
        uses: felixp8/dispatch-and-wait@v0.1.0
        with:
          owner: WordPress
          repo: openverse-infrastructure
          token: ${{ secrets.ACCESS_TOKEN }}
          event_type: deploy_production_${{ env.APP_NAME == 'frontend' && 'nuxt' || env.APP_NAME }}
          client_payload: |
            {
              "actor": "${{ github.actor }}",
              "tag": "${{ steps.tag.outputs.image-tag }}"
            }
          wait_time: 60 # check every minute
          max_time: 1800 # allow up to 30 minutes for a deployment
          # max_time can't easily be configured per application
          # without duplicating information between our infrastructure
          # and this workflow. The upstream workflows already have
          # timeouts appropriately configured and will all fail before 30
          # minutes is reached. On the other hand, we do want to wait
          # so that there is a record of the successful deployment.

      - name: Add new changelog file to documentation
        env:
          APP: ${{ env.APP_NAME }}
          DATE: ${{ steps.tag.outputs.date }}
          RELEASE_BODY: ${{ github.event.release.body }}
        working-directory: automations/python/workflows
        run: python write_changelog.py

      - name: Setup CI env
        uses: ./.github/actions/setup-env
        with:
          # Python is not needed to run pre-commit.
          setup_python: false
          # Node.js is needed by lint actions.
          install_recipe: "node-install"

      - name: Cache pre-commit envs
        uses: actions/cache@v4
        with:
          path: ~/.cache/pre-commit
          key: ${{ runner.os }}-pre-commit-${{ hashFiles('.pre-commit-config.yaml') }}

      - name: Lint the changelog file so that it passes CI
        run: |
          # Add the new changelog file to git so that pre-commit can lint it.
          git add documentation/changelogs/${{ env.APP_NAME }}/${{ steps.tag.outputs.date }}.md
          just precommit
          # Ensure this step passes even if linting has made changes so the workflow can continue
          just lint || true

      - name: Open changelog PR
        uses: peter-evans/create-pull-request@v6
        with:
          # Access token necessary for PRs to run with CI
          token: ${{ secrets.ACCESS_TOKEN }}
          base: main
          branch: changelog/${{ github.ref_name }}
          commit-message: Publish changelog for ${{ github.ref_name }}
          title: Publish changelog for ${{ github.ref_name }}
          # Add labels to pass CI
          labels: |
            🧱 stack: ${{ env.APP_NAME == 'ingestion_server' && 'ingestion server' || env.APP_NAME }}
            🌟 goal: addition
            📄 aspect: text
            🟩 priority: low
            skip-changelog
          body: |
            This changelog PR was automatically generated for @${{ github.actor }} as a result of the ${{ github.workflow }} workflow.


Project Thread Update Reminders

name: Project Thread Update Reminders

on:
  schedule:
    # Run every day at 00:00 UTC
    - cron: "0 0 * * *"

jobs:
  send-reminders:
    runs-on: ubuntu-latest
    # Prevent running this workflow on forks, it's unnecessary for external contributors
    if: github.repository_owner == 'WordPress'
    steps:
      - uses: actions/checkout@v4
      - name: Send reminders to update project threads
        uses: actions/github-script@v7
        with:
          github-token: ${{ secrets.ACCESS_TOKEN }}
          script: |
            const script = require('./automations/js/src/project_thread_updates.js')
            await script({github, core})


PR Limit Reminders

name: PR Limit Reminders

on:
  pull_request_target:
    types: [opened, reopened, ready_for_review]

jobs:
  analyze-user-prs:
    name: Count user's open pull requests
    runs-on: ubuntu-latest
    outputs:
      pr_count: ${{ steps.count-prs.outputs.pr_count }}
      slack_id: ${{ steps.count-prs.outputs.slack_id }}
      should_alert: ${{ steps.count-prs.outputs.should_alert }}
    steps:
      - uses: actions/checkout@v4
      - name: Count user PRs
        uses: actions/github-script@v7
        id: count-prs
        env:
          GH_SLACK_USERNAME_MAP: ${{ secrets.GH_SLACK_USERNAME_MAP }}
        with:
          github-token: ${{ secrets.ACCESS_TOKEN }}
          script: |
            const script = require('./automations/js/src/count_user_reviewable_prs.js')
            await script({github, context, core})

  send_message:
    needs: analyze-user-prs
    name: Send Slack message
    if: needs.analyze-user-prs.outputs.pr_count >= 3
    runs-on: ubuntu-latest
    env:
      pr_count: ${{ needs.analyze-user-prs.outputs.pr_count }}
      slack_id: ${{ needs.analyze-user-prs.outputs.slack_id }}
      SLACK_WEBHOOK_URL: ${{ secrets.SLACK_DM_WEBHOOK_URL }}
      SLACK_WEBHOOK_TYPE: INCOMING_WEBHOOK
    steps:
      - name: Set required review count
        run: |
          echo "required_review_count=$(( pr_count * 2 ))" >> "$GITHUB_ENV"
      - name: Send notification
        id: slack
        uses: slackapi/slack-github-action@v1.26.0
        with:
          # Note: We cannot use the YAML folded chomping block syntax here (`>`) because
          # GitHub Actions does not support it (they only support a subset of YAML):
          # https://github.com/actions/runner/issues/418#issuecomment-612928525
          payload: |
            {
              "user": "${{ env.slack_id }}",
              "message": "Hi, Opener! \n You currently have ${{ env.pr_count }} Pull Request(s) open with requested reviews (totalling ${{ env.required_review_count }} required reviews). \n To help ease the review burden, increase review velocity for older PRs, and improve the equitable distribution of project maintenance tasks across the team, please consider reviewing this list of ways to contribute instead of working on new code contributions: \n https://docs.openverse.org/meta/maintainer_tasks.html"
            }