Swift

name: Swift

on:
  push:
    branches: ['main']
    paths: ['src/swift/**']
  pull_request:
    branches: ['main']
    paths: ['src/swift/**']

concurrency:
  group: ${{ github.workflow }}-${{ github.ref == 'refs/heads/main' && github.run_id || github.event.pull_request.number || github.ref }}
  cancel-in-progress: true

jobs:
  test:
    defaults:
        run:
            working-directory: src/swift/flwr
    name: Test
    runs-on: macos-14
    steps:
      - uses: fwal/setup-swift@v2
        with:
          swift-version: 5.10
      - uses: actions/checkout@v4
      - name: Run tests
        run: arch -x86_64 xcodebuild test -scheme flwr -destination 'platform=iOS Simulator,name=iPhone 15 Pro Max,OS=17.2'

  build_docs:
    runs-on: macos-14
    name: Build docs
    steps:
      - uses: fwal/setup-swift@v2
        with:
          swift-version: 5.10
      - uses: actions/checkout@v4
      - name: Build docs
        run: ./dev/build-swift-api-ref.sh

  deploy_docs:
    needs: "build_docs"
    if: ${{ github.ref == 'refs/heads/main' && github.repository == 'adap/flower' && !github.event.pull_request.head.repo.fork }}
    runs-on: macos-14
    name: Deploy docs
    steps:
      - uses: fwal/setup-swift@v2
        with:
          swift-version: 5.10
      - uses: actions/checkout@v4
      - name: Build and deploy docs
        env:
          AWS_DEFAULT_REGION: ${{ secrets. AWS_DEFAULT_REGION }}
          AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
          AWS_SECRET_ACCESS_KEY: ${{ secrets. AWS_SECRET_ACCESS_KEY }}
        run: ./dev/deploy-swift-docs.sh


name: E2E

on:
  push:
    branches:
      - main
  pull_request:
    branches:
      - main

concurrency:
  group: ${{ github.workflow }}-${{ github.ref == 'refs/heads/main' && github.run_id || github.event.pull_request.number || github.ref }}
  cancel-in-progress: true

env:
  FLWR_TELEMETRY_ENABLED: 0
  ARTIFACT_BUCKET: artifact.flower.ai

jobs:
  wheel:
    runs-on: ubuntu-22.04
    name: Build, test and upload wheel
    steps:
      - uses: actions/checkout@v4
      - name: Bootstrap
        uses: ./.github/actions/bootstrap
      - name: Install dependencies (mandatory only)
        run: python -m poetry install
      - name: Build wheel
        run: ./dev/build.sh
      - name: Test wheel
        run: ./dev/test-wheel.sh
      - name: Upload wheel
        if: ${{ github.repository == 'adap/flower' && !github.event.pull_request.head.repo.fork && github.actor != 'dependabot[bot]' }}
        id: upload
        env:
          AWS_DEFAULT_REGION: ${{ secrets.AWS_DEFAULT_REGION }}
          AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
          AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
        run: |
          cd ./dist
          echo "WHL_PATH=$(ls *.whl)" >> "$GITHUB_OUTPUT"
          sha_short=$(git rev-parse --short HEAD)
          echo "SHORT_SHA=$sha_short" >> "$GITHUB_OUTPUT"
          [ -z "${{ github.head_ref }}" ] && dir="${{ github.ref_name }}" || dir="pr/${{ github.head_ref }}"
          echo "DIR=$dir" >> "$GITHUB_OUTPUT"
          aws s3 cp --content-disposition "attachment" --cache-control "no-cache" ./ s3://${{ env.ARTIFACT_BUCKET }}/py/$dir/$sha_short --recursive
          aws s3 cp --content-disposition "attachment" --cache-control "no-cache" ./ s3://${{ env.ARTIFACT_BUCKET }}/py/$dir/latest --recursive
    outputs:
      whl_path: ${{ steps.upload.outputs.WHL_PATH }}
      short_sha: ${{ steps.upload.outputs.SHORT_SHA }}
      dir: ${{ steps.upload.outputs.DIR }}

  frameworks:
    runs-on: ubuntu-22.04
    timeout-minutes: 10
    needs: wheel
    # Using approach described here:
    # https://docs.github.com/en/actions/using-jobs/using-a-matrix-for-your-jobs
    strategy:
      matrix:
        include:
          - directory: bare

          - directory: bare-https

          - directory: bare-client-auth

          - directory: jax

          - directory: pytorch
            dataset: |
              from torchvision.datasets import CIFAR10
              CIFAR10('./data', download=True)

          - directory: tensorflow
            dataset: |
              import tensorflow as tf
              tf.keras.datasets.cifar10.load_data()

          - directory: tabnet
            dataset: |
              import tensorflow_datasets as tfds
              tfds.load(name='iris', split=tfds.Split.TRAIN)

          - directory: opacus
            dataset: |
              from torchvision.datasets import CIFAR10
              CIFAR10('./data', download=True)

          - directory: pytorch-lightning
            dataset: |
              from torchvision.datasets import MNIST
              MNIST('./data', download=True)

          - directory: scikit-learn
            dataset: |
              import openml
              openml.datasets.get_dataset(554)

          - directory: fastai
            dataset: |
              from fastai.vision.all import untar_data, URLs
              untar_data(URLs.MNIST)

          - directory: pandas
            dataset: |
              from pathlib import Path
              from sklearn.datasets import load_iris
              Path('data').mkdir(exist_ok=True)
              load_iris(as_frame=True)['data'].to_csv('./data/client.csv')

    name: Framework / ${{matrix.directory}}

    defaults:
      run:
        working-directory: e2e/${{ matrix.directory }}

    steps:
      - uses: actions/checkout@v4
      - name: Set up Python
        uses: actions/setup-python@v5
        with:
          python-version: 3.8
      - name: Install build tools
        run: |
            python -m pip install -U pip==23.3.1
        shell: bash
      # Using approach described here for Python location caching:
      # https://blog.allenai.org/python-caching-in-github-actions-e9452698e98d
      - name: Cache Python location
        id: cache-restore-python
        uses: actions/cache/restore@v4
        with:
          path: ${{ env.pythonLocation }}
          key: pythonloc-${{ runner.os }}-${{ matrix.directory }}-${{ env.pythonLocation }}-${{ hashFiles(format('./e2e/{0}/pyproject.toml', matrix.directory)) }}
      - name: Install dependencies
        run: python -m pip install --upgrade .
      - name: Install Flower wheel from artifact store
        if: ${{ github.repository == 'adap/flower' && !github.event.pull_request.head.repo.fork && github.actor != 'dependabot[bot]' }}
        run: |
          python -m pip install https://${{ env.ARTIFACT_BUCKET }}/py/${{ needs.wheel.outputs.dir }}/${{ needs.wheel.outputs.short_sha }}/${{ needs.wheel.outputs.whl_path }}
      - name: Download dataset
        if: ${{ matrix.dataset }}
        run: python -c "${{ matrix.dataset }}"
      - name: Run edge client test
        if: ${{ matrix.directory != 'bare-client-auth' }}
        run: ./../test.sh "${{ matrix.directory }}"
      - name: Run virtual client test
        if: ${{ matrix.directory != 'bare-client-auth' }}
        run: python simulation.py
      - name: Run simulation engine test
        if: ${{ matrix.directory == 'pytorch' || matrix.directory == 'tensorflow'}}
        run: python simulation_next.py
      - name: Run driver test
        if: ${{ matrix.directory != 'bare-client-auth' }}
        run: ./../test_driver.sh "${{ matrix.directory }}"
      - name: Run driver test with REST
        if: ${{ matrix.directory == 'bare' }}
        run: ./../test_driver.sh bare rest
      - name: Run driver test with SQLite database
        if: ${{ matrix.directory == 'bare' }}
        run: ./../test_driver.sh bare sqlite
      - name: Run driver test with client authentication
        if: ${{ matrix.directory == 'bare-client-auth' }}
        run: ./../test_driver.sh bare client-auth
      - name: Cache save Python location
        id: cache-save-python
        uses: actions/cache/save@v4
        if: ${{ github.ref_name == 'main' &&  !steps.cache-restore-python.outputs.cache-hit }}
        with:
          path: ${{ env.pythonLocation }}
          key: pythonloc-${{ runner.os }}-${{ matrix.directory }}-${{ env.pythonLocation }}-${{ hashFiles(format('./e2e/{0}/pyproject.toml', matrix.directory)) }}

  strategies:
    runs-on: ubuntu-22.04
    timeout-minutes: 10
    needs: wheel
    strategy:
      matrix:
        strat: ["FedMedian", "FedTrimmedAvg", "QFedAvg", "FaultTolerantFedAvg", "FedAvgM", "FedAdam", "FedAdagrad", "FedYogi"]

    name: Strategy / ${{ matrix.strat }}

    defaults:
      run:
        working-directory: e2e/strategies

    steps:
      - uses: actions/checkout@v4
      - name: Bootstrap
        uses: ./.github/actions/bootstrap
      - name: Install dependencies
        run: |
          python -m poetry install
      - name: Install Flower wheel from artifact store
        if: ${{ github.repository == 'adap/flower' && !github.event.pull_request.head.repo.fork && github.actor != 'dependabot[bot]' }}
        run: |
          python -m pip install https://${{ env.ARTIFACT_BUCKET }}/py/${{ needs.wheel.outputs.dir }}/${{ needs.wheel.outputs.short_sha }}/${{ needs.wheel.outputs.whl_path }}
      - name: Cache Datasets
        uses: actions/cache@v4
        with:
          path: "~/.keras"
          key: keras-datasets
      - name: Download Datasets
        run: |
          python -c "import tensorflow as tf; tf.keras.datasets.mnist.load_data()"
      - name: Test strategies
        run: |
          python test.py "${{ matrix.strat }}"


Sync flower-swift

name: Sync flower-swift

on:
  push:
    branches: ['main']
    paths: ['src/swift/flwr/**']

concurrency:
  group: ${{ github.workflow }}-${{ github.ref == 'refs/heads/main' && github.run_id || github.event.pull_request.number || github.ref }}
  cancel-in-progress: true

jobs:
  build:
    runs-on: ubuntu-latest
    if: github.repository == 'adap/flower'
    steps:
      - uses: actions/checkout@v4
      - name: Pushes src/swift to flower-swift repository
        uses: cpina/github-action-push-to-another-repository@cbe757f8d8b380dd2b3ceec5a80fbcd61f1f3107
        env:
          SSH_DEPLOY_KEY: ${{ secrets.FLOWER_SWIFT_SSH }}
        with:
          source-directory: 'src/swift/flwr'
          destination-github-username: 'adap'
          destination-repository-name: 'flower-swift'


Deprecated-Baselines

name: Deprecated-Baselines

on:
  push:
    branches: ['main']
    paths: ['baselines/flwr_baselines/**']
  pull_request:
    branches: ['main']
    paths: ['baselines/flwr_baselines/**']

concurrency:
  group: ${{ github.workflow }}-${{ github.ref == 'refs/heads/main' && github.run_id || github.event.pull_request.number || github.ref }}
  cancel-in-progress: true

env:
  FLWR_TELEMETRY_ENABLED: 0

defaults:
  run:
    working-directory: baselines/flwr_baselines

jobs:
  test_deprecated_baselines:
    name: Test
    runs-on: ubuntu-22.04
    steps:
      - uses: actions/checkout@v4
      - name: Bootstrap
        uses: ./.github/actions/bootstrap
        with:
          python-version: 3.8
      - name: Install dependencies
        run: |
          python -m poetry install
      - name: Lint + Test (isort/black/mypy/pylint/pytest)
        run: ./dev/test.sh


Framework

name: Framework

on:
  push:
    branches:
      - main
  pull_request:
    branches:
      - main

concurrency:
  group: ${{ github.workflow }}-${{ github.ref == 'refs/heads/main' && github.run_id || github.event.pull_request.number || github.ref }}
  cancel-in-progress: true

env:
  FLWR_TELEMETRY_ENABLED: 0

jobs:
  test_core:
    runs-on: ubuntu-22.04
    strategy:
      matrix:
        # Latest version which comes cached in the host image can be found here:
        # https://github.com/actions/runner-images/blob/main/images/linux/Ubuntu2204-Readme.md#python
        # In case of a mismatch, the job has to download Python to install it.
        # Note: Due to a bug in actions/setup-python we have to put 3.10 in
        # qoutes as it will otherwise will assume 3.1
        python: [3.8, 3.9, '3.10', '3.11']

    name: Python ${{ matrix.python }}

    steps:
      - uses: actions/checkout@v4
        with:
          fetch-depth: 0
      - name: Bootstrap
        uses: ./.github/actions/bootstrap
        with:
          python-version: ${{ matrix.python }}
      - name: Install dependencies (mandatory only)
        run: python -m poetry install --all-extras
      - name: Check if protos need recompilation
        run: ./dev/check-protos.sh
      - name: Lint + Test (isort/black/docformatter/mypy/pylint/flake8/pytest)
        run: ./dev/test.sh


DevTools

name: DevTools

on:
  push:
    branches:
      - main
    paths:
      - "src/py/flwr_tool/**"
  pull_request:
    branches:
      - main
    paths:
      - "src/py/flwr_tool/**"

concurrency:
  group: ${{ github.workflow }}-${{ github.ref == 'refs/heads/main' && github.run_id || github.event.pull_request.number || github.ref }}
  cancel-in-progress: true

env:
  FLWR_TELEMETRY_ENABLED: 0

jobs:
  test_tools:
    runs-on: ubuntu-22.04
    name: Test
    steps:
      - uses: actions/checkout@v4
      - name: Bootstrap
        uses: ./.github/actions/bootstrap
      - name: Install dependencies (mandatory + optional)
        run: |
          python -m poetry install
      - name: Lint + Test (isort/black/mypy/pylint/pytest)
        run: ./dev/test-tool.sh


Android Client Publish

name: Android Client Publish

on:
  release:
    # We'll run this workflow when a new GitHub release is created
    types: [released]

concurrency:
  group: ${{ github.workflow }}-${{ github.ref == 'refs/heads/main' && github.run_id || github.event.pull_request.number || github.ref }}
  cancel-in-progress: true

jobs:
  publish:
    defaults:
        run:
            working-directory: src/kotlin
    name: Release build and publish
    if: github.repository == 'adap/flower'
    runs-on: ubuntu-22.04
    steps:
      - name: Check out code
        uses: actions/checkout@v4
      - name: Set up JDK 11
        uses: actions/setup-java@v3
        with:
          distribution: adopt
          java-version: 11

        # Runs upload, and then closes & releases the repository
      - name: Publish to MavenCentral
        run: ./gradlew publishAllPublicationsToMavenCentral --no-configuration-cache
        env:
          ORG_GRADLE_PROJECT_mavenCentralUsername: ${{ secrets.GRADLE_OSSRH_USERNAME }}
          ORG_GRADLE_PROJECT_mavenCentralPassword: ${{ secrets.GRADLE_OSSRH_PASSWORD }}
          ORG_GRADLE_PROJECT_signingInMemoryKeyId: ${{ secrets.GRADLE_SIGNING_KEY_ID }}
          ORG_GRADLE_PROJECT_signingInMemoryKeyPassword: ${{ secrets.GRADLE_SIGNING_PASSWORD }}
          ORG_GRADLE_PROJECT_signingInMemoryKey: ${{ secrets.GRADLE_SIGNING_KEY }}


Datasets

name: Datasets

on:
  push:
    branches:
      - main
    paths:
      - "datasets/**"
  pull_request:
    branches:
      - main
    paths:
      - "datasets/**"

concurrency:
  group: ${{ github.workflow }}-${{ github.ref == 'refs/heads/main' && github.run_id || github.event.pull_request.number || github.ref }}
  cancel-in-progress: true

env:
  FLWR_TELEMETRY_ENABLED: 0

defaults:
  run:
    working-directory: datasets

jobs:
  test_core:
    runs-on: ubuntu-22.04
    strategy:
      matrix:
        # Latest version which comes cached in the host image can be found here:
        # https://github.com/actions/runner-images/blob/main/images/linux/Ubuntu2204-Readme.md#python
        # In case of a mismatch, the job has to download Python to install it.
        # Note: Due to a bug in actions/setup-python we have to put 3.10 in
        # qoutes as it will otherwise will assume 3.1
        python: [3.8, 3.9, '3.10']

    name: Python ${{ matrix.python }}

    steps:
      - uses: actions/checkout@v4
      - name: Bootstrap
        uses: ./.github/actions/bootstrap
        with:
          python-version: ${{ matrix.python }}
      - name: Install dependencies (mandatory only)
        run: python -m poetry install --all-extras
      - name: Test (formatting + unit tests)
        run: ./dev/test.sh


Release nightly

name: Release nightly

on:
  schedule:
    - cron: "0 23 * * *"

env:
  FLWR_TELEMETRY_ENABLED: 0

jobs:
  release-nightly:
    runs-on: ubuntu-22.04
    name: Relase nightly on PyPI
    if: github.repository == 'adap/flower'
    outputs:
      name: ${{ steps.release.outputs.name }}
      version: ${{ steps.release.outputs.version }}
      skip: ${{ steps.release.outputs.skip }}
      pip-version: ${{ steps.release.outputs.pip-version }}
      setuptools-version: ${{ steps.release.outputs.setuptools-version }}
    steps:
      - uses: actions/checkout@v4
      - name: Bootstrap
        id: bootstrap
        uses: ./.github/actions/bootstrap
      - name: Release nightly
        id: release
        env:
          PYPI_TOKEN: ${{ secrets.PYPI_TOKEN }}
        run: |
          RESULT=$(./dev/publish-nightly.sh)
          if [ "$RESULT" == "There were no commits in the last 24 hours." ]; then
            echo "skip=true" >> $GITHUB_OUTPUT
          fi

          echo "name=$(poetry version | awk {'print $1'})" >> $GITHUB_OUTPUT
          echo "version=$(poetry version -s)" >> $GITHUB_OUTPUT
          echo "pip-version=${{ steps.bootstrap.outputs.pip-version }}" >> "$GITHUB_OUTPUT"
          echo "setuptools-version=${{ steps.bootstrap.outputs.setuptools-version }}" >> "$GITHUB_OUTPUT"

  build-docker-base-images:
    name: Build nightly base images
    if: github.repository == 'adap/flower' && needs.release-nightly.outputs.skip != 'true'
    uses: ./.github/workflows/_docker-build.yml
    needs: release-nightly
    with:
      namespace-repository: flwr/base
      file-dir: src/docker/base/ubuntu
      build-args: |
        PIP_VERSION=${{ needs.release-nightly.outputs.pip-version }}
        SETUPTOOLS_VERSION=${{ needs.release-nightly.outputs.setuptools-version }}
        FLWR_VERSION=${{ needs.release-nightly.outputs.version }}
        FLWR_PACKAGE=${{ needs.release-nightly.outputs.name }}
      tags: |
        ${{ needs.release-nightly.outputs.version }}
        nightly
    secrets:
      dockerhub-user: ${{ secrets.DOCKERHUB_USERNAME }}
      dockerhub-token: ${{ secrets.DOCKERHUB_TOKEN }}

  build-docker-binary-images:
    name: Build nightly binary images
    if: github.repository == 'adap/flower' && needs.release-nightly.outputs.skip != 'true'
    uses: ./.github/workflows/_docker-build.yml
    needs: [release-nightly, build-docker-base-images]
    strategy:
      fail-fast: false
      matrix:
        images: [
          { repository: "flwr/superlink", file_dir: "src/docker/superlink" },
          { repository: "flwr/supernode", file_dir: "src/docker/supernode" },
          { repository: "flwr/serverapp", file_dir: "src/docker/serverapp" }
        ]
    with:
      namespace-repository: ${{ matrix.images.repository }}
      file-dir: ${{ matrix.images.file_dir }}
      build-args: BASE_IMAGE=${{ needs.release-nightly.outputs.version }}
      tags: |
        ${{ needs.release-nightly.outputs.version }}
        nightly
    secrets:
      dockerhub-user: ${{ secrets.DOCKERHUB_USERNAME }}
      dockerhub-token: ${{ secrets.DOCKERHUB_TOKEN }}


Publish `flwr` release on PyPI

name: Publish `flwr` release on PyPI

on:
  release:
    types: [released]

concurrency:
  group: ${{ github.workflow }}-${{ github.ref == 'refs/heads/main' && github.run_id || github.event.pull_request.number || github.ref }}
  cancel-in-progress: true

env:
  ARTIFACT_BUCKET: artifact.flower.ai

jobs:
  publish:
    if: ${{ github.repository == 'adap/flower' }}
    name: Publish release
    runs-on: ubuntu-22.04
    steps:
    - name: Checkout code
      uses: actions/checkout@v4
      with:
        fetch-depth: 0

    - name: Bootstrap
      uses: ./.github/actions/bootstrap

    - name: Get artifacts and publish
      env:
        GITHUB_REF: ${{ github.ref }}
      run: |
        TAG_NAME=$(echo "${GITHUB_REF_NAME}" | cut -c2-)

        wheel_name="flwr-${TAG_NAME}-py3-none-any.whl"
        tar_name="flwr-${TAG_NAME}.tar.gz"

        wheel_url="https://${{ env.ARTIFACT_BUCKET }}/py/release/v${TAG_NAME}/${wheel_name}"
        tar_url="https://${{ env.ARTIFACT_BUCKET }}/py/release/v${TAG_NAME}/${tar_name}"

        mkdir -p dist

        curl $wheel_url --output dist/$wheel_name
        curl $tar_url --output dist/$tar_name

        python -m poetry publish -u __token__ -p ${{ secrets.PYPI_TOKEN_RELEASE_FLWR }}


C++ SDK

name: C++ SDK

on:
  push:
    branches: ['main']
    paths: ['src/cc/flwr/**']
  pull_request:
    branches: ['main']
    paths: ['src/cc/flwr/**']

jobs:
  build_and_test:
    name: Build and test
    runs-on: ubuntu-22.04

    steps:
    - uses: actions/checkout@v4
    - name: Bootstrap
      uses: ./.github/actions/bootstrap

    - name: Cache restore SDK build
      uses: actions/cache/restore@v4
      with:
        path: build/
        key: ${{ runner.os }}-sdk-build

    - name: Cache restore example build
      uses: actions/cache/restore@v4
      with:
        path: examples/quickstart-cpp/build/
        key: ${{ runner.os }}-example-build

    - name: Install Dependencies
      run: |
        sudo apt-get update
        sudo apt-get install -y clang-format cmake g++ clang-tidy cppcheck

    - name: Check source Formatting
      run: |
        find src/cc/flwr/src -name '*.cc' | xargs clang-format -i
        git diff --exit-code

    - name: Check header Formatting
      run: |
        find src/cc/flwr/include -name '*.h' -not -path "src/cc/flwr/include/flwr/*" | xargs clang-format -i
        git diff --exit-code

    - name: Build
      run: |
        mkdir -p build
        cd build
        cmake -DCMAKE_EXPORT_COMPILE_COMMANDS=ON ../src/cc/flwr
        make

    - name: Run clang-tidy
      run: |
        cd build
        find ../src/cc/flwr/src -name '*.cc' | xargs clang-tidy

    - name: Run cppcheck
      run: |
        cd build
        cppcheck --enable=all -I../src/cc/flwr/include ../src/cc/flwr/src

    - name: End-to-end test
      run: |
        cd examples/quickstart-cpp
        cmake -DUSE_LOCAL_FLWR=ON -S . -B build
        cmake --build build
        pip install ../..
        timeout 3m flower-superlink --insecure &
        sleep 10
        timeout 2m build/flwr_client 0 127.0.0.1:9092 &
        sleep 3
        timeout 2m build/flwr_client 1 127.0.0.1:9092 &
        sleep 3
        flower-server-app server:app --insecure &
        pid=$!
        wait $pid
        res=$?
        if [[ "$res" = "0" ]];
          then echo "Training worked correctly" && exit 0;
          else echo "Training had an issue" && exit 1;
        fi

    - name: Cache save SDK build
      uses: actions/cache/save@v4
      if: github.ref_name == 'main'
      with:
        path: build/
        key: ${{ runner.os }}-sdk-build

    - name: Cache save example build
      uses: actions/cache/save@v4
      if: github.ref_name == 'main'
      with:
        path: examples/quickstart-cpp/build/
        key: ${{ runner.os }}-example-build


PR update

name: PR update

on:
  push:
    branches:
      - 'main'
jobs:
  autoupdate:
    runs-on: ubuntu-22.04
    steps:
      - name: Automatically update mergeable PRs
        uses: adRise/update-pr-branch@cd305ecbd76bf63056c9400ce2c725293fc3e0c0 # v0.7.0
        with:
          token: ${{ secrets.FLWRMACHINE_TOKEN }}
          base: 'main'
          required_approval_count: 1
          require_passed_checks: true
          sort: 'created'
          direction: 'desc'


Reusable docker image build workflow

name: Reusable docker image build workflow

on:
  workflow_call:
    inputs:
      namespace-repository:
        description: "The namespace and repository in the following format `namespace/repository` e.g. (flwr/base)."
        required: true
        type: string
      file-dir:
        description: "Path of the directory that contains the Dockerfile."
        required: true
        type: string
      build-args:
        description: "List of build-time variables."
        required: false
        type: string
      tags:
        description: "List of tags."
        required: true
        type: string
    secrets:
      dockerhub-user:
        required: true
      dockerhub-token:
        required: true
    outputs:
      metadata:
        description: "Metadata of the docker image."
        value: ${{ jobs.build-manifest.outputs.metadata }}

permissions:
  contents: read

# based on https://docs.docker.com/build/ci/github-actions/multi-platform/#distribute-build-across-multiple-runners
jobs:
  build:
    name: Build image
    runs-on: ubuntu-22.04
    timeout-minutes: 180
    outputs:
      build-id: ${{ steps.build-id.outputs.id }}
    strategy:
      fail-fast: true
      matrix:
        platform: [
            # build-push action and qemu use different platform names
            # therefore we create a map
            { name: "amd64", qemu: "", docker: "linux/amd64" },
            { name: "arm64", qemu: "arm64", docker: "linux/arm64" },
          ]
    steps:
      - name: Create build id
        id: build-id
        shell: python
        run: |
          import hashlib
          import os

          hash = hashlib.sha256('''${{ inputs.namespace-repository }}
          ${{ inputs.file-dir }}
          ${{ inputs.build-args }}'''.encode())
          # Adds two spaces to the line breaks to ensure proper indentation
          # when passing the multi-line string to the wretry.action.
          # Without it, the multi-line string is passed like this:
          #
          # build-args: |
          #   ARG1=
          # ARG2=
          # ARG3=
          #
          # This causes the Docker action to interpret ARG2 and ARG3 as keys instead
          # of values of the multi-line string.
          build_args = '''${{ inputs.build-args }}'''.replace("\n", "\n  ")

          with open(os.environ['GITHUB_OUTPUT'], 'a') as fh:
              print(f"id={hash.hexdigest()}", file=fh)
              print("build-args<<EOF", file=fh)
              print(build_args, file=fh)
              print("EOF", file=fh)

      - name: Set up QEMU
        if: matrix.platform.qemu != ''
        uses: docker/setup-qemu-action@68827325e0b33c7199eb31dd4e31fbe9023e06e3 # v3.0.0
        with:
          platforms: ${{ matrix.platform.qemu }}

      - name: Extract metadata (tags, labels) for Docker
        id: meta
        uses: docker/metadata-action@8e5442c4ef9f78752691e2d8f8d19755c6f78e81 # v5.5.1
        with:
          images: ${{ inputs.namespace-repository }}

      - name: Set up Docker Buildx
        uses: docker/setup-buildx-action@d70bba72b1f3fd22344832f00baa16ece964efeb # v3.3.0

      - name: Login to Docker Hub
        uses: docker/login-action@0d4c9c5ea7693da7b068278f7b52bda2a190a446 # v3.2.0
        with:
          username: ${{ secrets.dockerhub-user }}
          password: ${{ secrets.dockerhub-token }}

      - name: Build and push
        uses: Wandalen/wretry.action@6feedb7dedadeb826de0f45ff482b53b379a7844 # v3.5.0
        id: build
        with:
          action: docker/build-push-action@2cdde995de11925a030ce8070c3d77a52ffcf1c0 # v5.3.0
          attempt_limit: 60 # 60 attempts * (9 secs delay + 1 sec retry) = ~10 mins
          attempt_delay: 9000 # 9 secs
          with: |
            pull: true
            platforms: ${{ matrix.platform.docker }}
            context: "{{defaultContext}}:${{ inputs.file-dir }}"
            outputs: type=image,name=${{ inputs.namespace-repository }},push-by-digest=true,name-canonical=true,push=true
            build-args: |
              ${{ steps.build-id.outputs.build-args }}

      - name: Export digest
        run: |
          mkdir -p /tmp/digests
          digest="${{ fromJSON(steps.build.outputs.outputs).digest }}"
          touch "/tmp/digests/${digest#sha256:}"

      - name: Upload digest
        uses: actions/upload-artifact@65462800fd760344b1a7b4382951275a0abb4808 # v4.3.3
        with:
          name: digests-${{ steps.build-id.outputs.id }}-${{ matrix.platform.name }}
          path: /tmp/digests/*
          if-no-files-found: error
          retention-days: 1

  build-manifest:
    name: Build and push docker manifest for all platforms
    runs-on: ubuntu-22.04
    timeout-minutes: 10
    needs: build
    outputs:
      metadata: ${{ steps.meta.outputs.json }}
    steps:
      - name: Download digests
        uses: actions/download-artifact@65a9edc5881444af0b9093a5e628f2fe47ea3b2e # v4.1.7
        with:
          pattern: digests-${{ needs.build.outputs.build-id }}-*
          path: /tmp/digests
          merge-multiple: true

      - name: Docker meta
        id: meta
        uses: docker/metadata-action@8e5442c4ef9f78752691e2d8f8d19755c6f78e81 # v5.5.1
        with:
          images: ${{ inputs.namespace-repository }}
          tags: ${{ inputs.tags }}

      - name: Set up Docker Buildx
        uses: docker/setup-buildx-action@d70bba72b1f3fd22344832f00baa16ece964efeb # v3.3.0

      - name: Login to Docker Hub
        uses: docker/login-action@0d4c9c5ea7693da7b068278f7b52bda2a190a446 # v3.2.0
        with:
          username: ${{ secrets.dockerhub-user }}
          password: ${{ secrets.dockerhub-token }}

      - name: Create manifest list and push
        working-directory: /tmp/digests
        run: |
          docker buildx imagetools create $(jq -cr '.tags | map("-t " + .) | join(" ")' <<< "$DOCKER_METADATA_OUTPUT_JSON") \
              $(printf '${{ inputs.namespace-repository }}@sha256:%s ' *)
      - name: Inspect image
        run: docker buildx imagetools inspect ${{ inputs.namespace-repository }}:${{ steps.meta.outputs.version }}


name: Datasets-E2E

on:
  push:
    branches:
      - main
    paths:
      - "datasets/flwr_datasets/**"
  pull_request:
    branches:
      - main
    paths:
      - "datasets/flwr_datasets/**"

concurrency:
  group: ${{ github.workflow }}-${{ github.ref == 'refs/heads/main' && github.run_id || github.event.pull_request.number || github.ref }}
  cancel-in-progress: true

env:
  FLWR_TELEMETRY_ENABLED: 0

jobs:
  frameworks:
    runs-on: ubuntu-22.04
    timeout-minutes: 10
    # Using approach described here:
    # https://docs.github.com/en/actions/using-jobs/using-a-matrix-for-your-jobs
    strategy:
      matrix:
        include:
          - directory: pytorch

          - directory: tensorflow

          - directory: scikit-learn

    name: Framework / ${{matrix.directory}}

    defaults:
      run:
        working-directory: datasets/e2e/${{ matrix.directory }}

    steps:
      - uses: actions/checkout@v4
      - name: Bootstrap
        uses: ./.github/actions/bootstrap
        with:
          python-version: 3.8
      - name: Install dependencies
        run: python -m poetry install
      - name: Run tests
        run: python -m unittest discover -p '*_test.py'


Draft release

name: Draft release

on:
  push:
    tags:
      - "v*.*.*"

env:
  ARTIFACT_BUCKET: artifact.flower.ai

jobs:
  publish:
    if: ${{ github.repository == 'adap/flower' }}
    name: Publish draft
    runs-on: ubuntu-22.04
    steps:
      - name: Checkout
        uses: actions/checkout@v4
        with:
          fetch-depth: 0
      - name: Wait for wheel to be built
        uses: lewagon/wait-on-check-action@v1.3.4
        with:
          ref: ${{ github.ref }}
          check-name: 'Build, test and upload wheel'
          repo-token: ${{ secrets.GITHUB_TOKEN }}
          wait-interval: 10
      - name: Download wheel
        run: |
          tag_name=$(echo "${GITHUB_REF_NAME}" | cut -c2-)
          echo "TAG_NAME=$tag_name" >> "$GITHUB_ENV"

          wheel_name="flwr-${tag_name}-py3-none-any.whl"
          echo "WHEEL_NAME=$wheel_name" >> "$GITHUB_ENV"

          tar_name="flwr-${tag_name}.tar.gz"
          echo "TAR_NAME=$tar_name" >> "$GITHUB_ENV"

          wheel_url="https://${{ env.ARTIFACT_BUCKET }}/py/main/${GITHUB_SHA::7}/${wheel_name}"
          tar_url="https://${{ env.ARTIFACT_BUCKET }}/py/main/${GITHUB_SHA::7}/${tar_name}"

          curl $wheel_url --output $wheel_name
          curl $tar_url --output $tar_name
      - name: Upload wheel
        env:
          AWS_DEFAULT_REGION: ${{ secrets. AWS_DEFAULT_REGION }}
          AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
          AWS_SECRET_ACCESS_KEY: ${{ secrets. AWS_SECRET_ACCESS_KEY }}
        run: |
          aws s3 cp --content-disposition "attachment" --cache-control "no-cache" ./${{ env.WHEEL_NAME }} s3://${{ env.ARTIFACT_BUCKET }}/py/release/v${{ env.TAG_NAME }}/${{ env.WHEEL_NAME }}
          aws s3 cp --content-disposition "attachment" --cache-control "no-cache" ./${{ env.TAR_NAME }} s3://${{ env.ARTIFACT_BUCKET }}/py/release/v${{ env.TAG_NAME }}/${{ env.TAR_NAME }}

      - name: Generate body
        run: |
          ./dev/get-latest-changelog.sh > body.md
          cat body.md

      - name: Release
        uses: softprops/action-gh-release@v1
        with:
          body_path: ./body.md
          draft: true
          name: Flower ${{ env.TAG_NAME }}
          files: |
            ${{ env.WHEEL_NAME }}
            ${{ env.TAR_NAME }}


PR Check

name: PR Check

on:
  pull_request:
    types: [opened, synchronize, reopened, edited]
    branches:
      - main

concurrency:
  group: ${{ github.workflow }}-${{ github.ref == 'refs/heads/main' && github.run_id || github.event.pull_request.number || github.ref }}
  cancel-in-progress: true

jobs:
  pr_title_check:
    runs-on: ubuntu-22.04
    name: Title format
    steps:
      - uses: actions/checkout@v4
      - name: Bootstrap
        uses: ./.github/actions/bootstrap
        with:
          python-version: 3.11
          poetry-skip: 'true'
      - name: Check PR title format
        run: python ./dev/check_pr_title.py "${{ github.event.pull_request.title }}"


Cache cleanup

name: Cache cleanup

on:
  workflow_dispatch

jobs:
  e2e-cleanup:
    runs-on: ubuntu-22.04
    timeout-minutes: 10
    # Using approach described here:
    # https://docs.github.com/en/actions/using-jobs/using-a-matrix-for-your-jobs
    strategy:
      matrix:
        include:
          - directory: bare
          - directory: bare-https
          - directory: bare-client-auth
          - directory: jax
          - directory: pytorch
          - directory: tensorflow
          - directory: tabnet
          - directory: opacus
          - directory: pytorch-lightning
          - directory: scikit-learn
          - directory: fastai
          - directory: pandas

    name: Framework / ${{ matrix.directory }}

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

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

      - name: Cleanup caches by directories
        # Only keep caches that match the latest keys for each directory
        run: |
          gh extension install actions/gh-actions-cache

          REPO=${{ github.repository }}
          LATEST_KEY=pythonloc-${{ matrix.directory }}-${{ env.pythonLocation }}-${{ hashFiles(format('./e2e/{0}/pyproject.toml', matrix.directory)) }}

          echo "Fetching list of cache keys"
          cacheKeys=$(gh actions-cache list -R $REPO | grep "${{ matrix.directory }}" | cut -f 1 )

          ## Setting this to not fail the workflow while deleting cache keys.
          set +e
          echo "Deleting caches..."
          for cacheKey in $cacheKeys
          do
            if [ "$cacheKey" != "$LATEST_KEY" ]; then
              echo -e "\tOld key found -> $cacheKey"
              gh actions-cache delete "$cacheKey" --confirm
            fi
          done

          echo "Done"
        env:
          GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}


format check

name: format check

on: [push, pull_request]

jobs:
  build:
    runs-on: ubuntu-22.04
    steps:
      - uses: actions/checkout@v2
      - name: Setup Python.3.9.x
        uses: actions/setup-python@v1
        with:
          python-version: 3.9.x
      - name: Cache pip
        uses: actions/cache@v1
        with:
          path: ~/.cache/pip
          key: ${{ runner.os }}-pip-${{ hashFiles('requirements.txt') }}
          restore-keys: |
            ${{ runner.os }}-pip-
            ${{ runner.os }}-
      - name: Install packages
        run: |
          python -m pip install --upgrade pip
          pip install Cython numpy
          pip install -e .
          pip install -r dev.requirements.txt
      - name: Check format
        run: |
          ./scripts/format
      - name: Linter
        run: |
          ./scripts/lint

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


test

name: test

on: [push, pull_request]

jobs:
  build:
    runs-on: ${{ matrix.os }}
    strategy:
      matrix:
        # temporary drop windows test
        # os: [ubuntu-22.04, macos-latest, windows-latest]
        os: [ubuntu-22.04, macos-latest]
    steps:
      - uses: actions/checkout@v4
      - name: Setup Python.3.10.x
        uses: actions/setup-python@v5
        with:
          python-version: '3.10'
      - name: Cache pip
        uses: actions/cache@v1
        with:
          path: ~/.cache/pip
          key: ${{ runner.os }}-pip-${{ hashFiles('requirements.txt') }}
          restore-keys: |
            ${{ runner.os }}-pip-
            ${{ runner.os }}-
      - name: Install dependencies for Windows
        if: ${{ matrix.os == 'windows-latest' }}
        run: |
          pip install torch==1.13.1+cpu -f https://download.pytorch.org/whl/torch_stable.html
      - name: Install dependencies for macOS
        if: ${{ matrix.os == 'macos-latest' }}
        run: |
          brew install libomp
      - name: Install packages
        run: |
          python -m pip install --upgrade pip
          pip install Cython numpy
          pip install -e .
          pip install -r dev.requirements.txt
      - name: Unit tests
        run: |
          mkdir -p test_data
          pytest --cov-report=xml --cov=d3rlpy --cov-config=.coveragerc tests -p no:warnings -v
      - name: Upload coverage
        if: ${{ matrix.os == 'ubuntu-22.04' }}
        env:
          CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
        run: |
          bash <(curl -s https://codecov.io/bash)

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


name: docker_build

on:
  push:
    branches:
      - '**'

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

      -
        name: Docker meta
        id: docker_meta
        uses: crazy-max/ghaction-docker-meta@v1
        with:
          images: leogao2/gpt-neox # list of Docker images to use as base name for tags
          tag-sha: true # add git short SHA as Docker tag

      -
        name: Set up QEMU
        uses: docker/setup-qemu-action@v1

      -
        name: Set up Docker Buildx
        uses: docker/setup-buildx-action@v1

      -
        name: Login to DockerHub
        uses: docker/login-action@v1
        with:
          username: ${{ secrets.DOCKERHUB_USERNAME }}
          password: ${{ secrets.DOCKERHUB_TOKEN }}

      -
        name: Build and push
        id: docker_build
        uses: docker/build-push-action@v2
        with:
          push: ${{ github.event_name != 'pull_request' }}
          tags: ${{ steps.docker_meta.outputs.tags }}
          labels: ${{ steps.docker_meta.outputs.labels }}

      -
        name: Image digest
        run: echo ${{ steps.docker_build.outputs.digest }}


Pull Request

name: Pull Request

on: [pull_request, workflow_dispatch]

jobs:
  pre-commit:
    runs-on: ubuntu-22.04
    steps:
      - uses: actions/checkout@v2
      - uses: actions/setup-python@v4
        with:
          python-version: "3.10.14"
          cache: "pip"
          cache-dependency-path: "**/requirements*.txt"
      # Need the right version of clang-format
      - run: pip install -r requirements/requirements-dev.txt
      - uses: pre-commit/action@v2.0.3
      -
        name: Set up Docker Buildx
        uses: docker/setup-buildx-action@v1
      -
        name: Docker build
        id: docker_build
        uses: docker/build-push-action@v2

  update-documentation:
    runs-on: ubuntu-22.04
    steps:
      - uses: actions/checkout@v3
        with:
          ref: ${{ github.event.pull_request.head.ref}}
      - run: |
          rm megatron/__init__.py
          pip install shortuuid
          rm megatron/neox_arguments/__init__.py
          python configs/gen_docs.py
          git config user.name github-actions
          git config user.email github-actions@github.com
          git add configs/neox_arguments.md
          git commit -m "Update NeoXArgs docs automatically"
          git push
  run-tests:
    runs-on: self-hosted
    steps:
      - uses: actions/checkout@v2
      - uses: actions/setup-python@v4
        with:
          python-version: "3.10.13"
          cache-dependency-path: "**/requirements*.txt"
      - name: prepare data
        run: python3 prepare_data.py
      - name: install pytest
        run: python3 -m pip install pytest pytest-forked pyyaml requests wandb
      - name: install torch
        run: python3 -m pip install torch
      - name: install requirements
        run: pip install -r requirements/requirements.txt
      - name: Run Tests
        run: pytest --forked tests


Run CPU Tests

name: "Run CPU Tests"

on: "push"

jobs:
  run-tests:
    #runs-on: ubuntu-latest
    runs-on: [ 'test', 'self-hosted' ]
    steps:
      - uses: actions/checkout@v3

      - name: Install Python
        uses: actions/setup-python@v4
        with:
          python-version: "3.8"
          cache: "pip"
          cache-dependency-path: "**/requirements*.txt"

      - name: Upgrade Pip
        run: python -m pip install --upgrade pip

      - name: Install Dependencies
        run: |
          sudo apt-get install libopenmpi-dev -y
          pip install torch==1.8.2 torchvision==0.9.2 torchaudio==0.8.2 --extra-index-url https://download.pytorch.org/whl/lts/1.8/cpu
          pip install -r requirements/requirements.txt
          pip install -r requirements/requirements-dev.txt
          pip install -r requirements/requirements-wandb.txt

      - name: Prepare Data
        run: python prepare_data.py

      - name: Run CPU Tests
        run: PROTOCOL_BUFFERS_PYTHON_IMPLEMENTATION=python pytest tests -m cpu


Coverity

name: Coverity
on:
  workflow_dispatch:
    inputs:
      build_version:
        description: "Version of GPT-NeoX being submitted for scan"
        required: false
        default: "GPT-NeoX build version"
      build_description:
        description: "Description of the current build"
        required: false
        default: "Current build of GPT-NeoX"

jobs:
  coverity:

    runs-on: ubuntu-latest

    env:
      COV_USER: ${{ secrets.COV_USER }}
      COVERITY_PROJECT: ${{ secrets.COVERITY_PROJECT }}
      COVERITY_TOKEN: ${{ secrets.COVERITY_TOKEN }}

    steps:
    - uses: actions/checkout@v2
      with:
        path: gpt-neox

    - name: Install utils
      run: |
        sudo apt update -y && sudo apt upgrade -y
        sudo apt install curl jq wget -y

    - name: Coverity Download
      run: |
        wget https://scan.coverity.com/download/linux64 --post-data "token=$COVERITY_TOKEN&project=$COVERITY_PROJECT" -O coverity_tool.tgz --no-verbose
        mkdir $GITHUB_WORKSPACE/coverity && tar xvf coverity_tool.tgz -C $GITHUB_WORKSPACE/coverity --strip-components=1
        $GITHUB_WORKSPACE/coverity/bin/cov-configure --python
        $GITHUB_WORKSPACE/coverity/bin/cov-configure --gcc

    - name: Coverity Scan and Upload
      run: |
        set -x
        pushd $GITHUB_WORKSPACE
        cd $GITHUB_WORKSPACE/gpt-neox
        $GITHUB_WORKSPACE/coverity/bin/cov-build --dir $GITHUB_WORKSPACE/cov-int --no-command --fs-capture-search ./
        popd
        tar caf build-results.bz2 cov-int
        curl --form token=$COVERITY_TOKEN \
          --form email=$COV_USER \
          --form file=@build-results.bz2 \
          --form version="${{ inputs.build_version }}" \
          --form description="${{ inputs.build_description }}" \
          https://scan.coverity.com/builds?project=$COVERITY_PROJECT

    - name: Upload Scan Build as Artifact
      uses: actions/upload-artifact@v3
      with:
        name: coverity-build-${{ github.sha }}
        path: build-results.bz2


Pull Request CPU Tests

name: "Pull Request CPU Tests"

on:
  pull_request:
    paths: # job only triggers when the PR changes files under megatron directory
      - "megatron/**"

jobs:
  run-tests:
    runs-on: [ 'test', 'self-hosted' ]
    steps:
    - name: Checkout Repository
      uses: actions/checkout@v4
    - name: Run CPU tests
      uses: ./tests/cpu_tests
      with:
        target_test_ref: ${{ github.event.pull_request.base.sha }}


Workflow Dispatch CPU Tests

name: "Workflow Dispatch CPU Tests"

on:
  workflow_dispatch:
    inputs:
      ref:
        description: 'Target ref / SHA to run tests against'
        required: true
        default: 'main'

jobs:
  run-tests:
    runs-on: [ 'test', 'self-hosted' ]
    steps:
    - name: Checkout Repository
      uses: actions/checkout@v4
    - name: Run CPU tests
      uses: ./tests/cpu_tests
      with:
        target_test_ref: ${{ inputs.ref }}


Pull Request Labeler

name: "Pull Request Labeler"
on:
- pull_request_target

jobs:
  triage:
    permissions:
      contents: read
      pull-requests: write
    runs-on: ubuntu-latest
    steps:
    - uses: actions/labeler@v4


name: Only allow staging -> prod & !prod -> staging merges

on:
  pull_request:
    branches:
    - staging
    - prod

jobs:
  check-branch:
    runs-on: ubuntu-latest
    steps:
    - name: Only allow staging -> prod merges
      run: |
        # All branches *except* prod can be merged into staging
        # *Only* staging can be merged into prod
        echo "Merging ${GITHUB_HEAD_REF} into ${GITHUB_BASE_REF}"
        python3 -c 'import os, sys; sys.exit(not(os.environ["GITHUB_HEAD_REF"] == "staging" if os.environ["GITHUB_BASE_REF"] == "prod" else os.environ["GITHUB_HEAD_REF"] != "prod"))'


CodeQL

name: "CodeQL"

on:
  push:
    branches: [ master, "ci/*" ]
  pull_request:
    # The branches below must be a subset of the branches above
    branches: [ master ]
  schedule:
    - cron: '16 16 * * 5'

permissions:
  actions: read
  contents: read
jobs:
  analyze:
    name: Analyze
    runs-on: ubuntu-latest
    permissions:
      actions: read
      contents: read
      security-events: write

    strategy:
      fail-fast: false
      matrix:
        language: [ 'python' ]

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

    # Initializes the CodeQL tools for scanning.
    - name: Initialize CodeQL
      uses: github/codeql-action/init@v3
      with:
        languages: ${{ matrix.language }}
        # If you wish to specify custom queries, you can do so here or in a config file.
        # By default, queries listed here will override any specified in a config file.
        # Prefix the list here with "+" to use these queries and those in the config file.
        # queries: ./path/to/local/query, your-org/your-repo/queries@main

    # Autobuild attempts to build any compiled languages  (C/C++, C#, or Java).
    # If this step fails, then you should remove it and run the build manually (see below)
    - name: Autobuild
      uses: github/codeql-action/autobuild@v3

    # ℹ Command-line programs to run using the OS shell.
    # 📚 https://git.io/JvXDl

    # ✏ If the Autobuild fails above, remove it and uncomment the following three lines
    #    and modify them (or add more) to build your code if your project
    #    uses a compiled language

    #- run: |
    #   make bootstrap
    #   make release

    - name: Perform CodeQL Analysis
      uses: github/codeql-action/analyze@v3


CI pipeline

name: CI pipeline

on:
  push:
    branches: [ master, "release/*", "maintenance/*", "ci/*" ]
  pull_request:
    branches: [ master ]
  workflow_call:
    secrets:
      CODECOV_TOKEN:
        required: true
    outputs:
      hashes:
        description: "Hashes of the artifacts that were built"
        value: ${{ jobs.build.outputs.hashes }}
  workflow_dispatch: {}
permissions:
  actions: read
  contents: read
env:
  MAIN_PYTHON_VERSION: "3.10"
  PDFTOPPM_PATH: /usr/bin/pdftoppm
  IM_COMPARE_PATH: /usr/bin/compare
  SOFTHSM2_CONF: /tmp/softhsm2.conf
  SOFTHSM2_MODULE_PATH: /usr/lib/softhsm/libsofthsm2.so
  CERTOMANCER_CONFIG_PATH: pyhanko_tests/data/crypto/certomancer.yml

jobs:
  build:
    runs-on: ubuntu-latest
    outputs:
      hashes: ${{ steps.artifact-hashes.outputs.hashes }}
    steps:
      - uses: actions/checkout@v4
      - name: Set up Python
        uses: actions/setup-python@v5
        with:
          python-version: ${{ env.MAIN_PYTHON_VERSION }}
      - name: Install build tools
        run: pip install --upgrade build setuptools pip wheel
      - name: Build release artifacts
        run: python -m build
      - name: Record release artifact hashes
        id: artifact-hashes
        run: cd dist && echo "hashes=$(sha256sum * | base64 -w0)" >> "$GITHUB_OUTPUT"
      - name: Upload dist artifacts
        uses: actions/upload-artifact@v4
        with:
          name: pyhanko-dist
          path: dist/
  pytest-coverage:
    runs-on: ubuntu-latest
    needs: build
    strategy:
      matrix:
        python-version: ["3.8", "3.9", "3.10", "3.11", "3.12"]
    steps:
      - uses: actions/checkout@v4
      - name: Set up Python ${{ matrix.python-version }}
        uses: actions/setup-python@v5
        with:
          python-version: ${{ matrix.python-version }}
      - name: Download dist artifacts
        uses: actions/download-artifact@v4
        with:
          name: pyhanko-dist
          path: dist/
      - uses: ./.github/actions/test-job-setup
      - name: Test with pytest
        run: python -m pytest --cov=./ --cov-report=xml:python-${{ matrix.python-version }}-coverage.xml
        env:
          PKCS11_TEST_MODULE: ${{ env.SOFTHSM2_MODULE_PATH }}
      - name: Stash coverage report
        uses: actions/upload-artifact@v4
        with:
          name: coverage-${{ strategy.job-index }}
          path: "*-coverage.xml"
  live-integration-tests:
    runs-on: ubuntu-latest
    needs: build
    strategy:
      matrix:
        python-version: ["3.8", "3.9", "3.10", "3.11", "3.12"]
    steps:
      - uses: actions/checkout@v4
      - name: Set up Python ${{ matrix.python-version }}
        uses: actions/setup-python@v5
        with:
          python-version: ${{ matrix.python-version }}
      - name: Download dist artifacts
        uses: actions/download-artifact@v4
        with:
          name: pyhanko-dist
          path: dist/
      - uses: ./.github/actions/test-job-setup
        with:
          dependency-group: live-test
      - name: Start Certomancer Animator daemon
        run: |
          certomancer --service-url-prefix http://localhost:9000 \
            --config "$CERTOMANCER_CONFIG_PATH" animate &
      - name: Start CSC dummy server
        run: |
          certomancer-csc "$CERTOMANCER_CONFIG_PATH" 8999 2 &
      - name: Test with pytest
        run: |
          python -m pytest --cov=./ --cov-report=xml:python-${{ matrix.python-version }}-live-coverage.xml \
                pyhanko_tests/with_live_certomancer.py \
                pyhanko_tests/with_live_csc_dummy.py
        env:
          LIVE_CERTOMANCER_HOST_URL: http://localhost:9000
          LIVE_CSC_SCAL2_HOST_URL: http://localhost:8999
      - name: Stash coverage report
        uses: actions/upload-artifact@v4
        with:
          name: coverage-live-${{ strategy.job-index }}
          path: "*-coverage.xml"
  smoke-tests:
    runs-on: ubuntu-latest
    needs: build
    steps:
      - uses: actions/checkout@v4
      - name: Set up Python
        uses: actions/setup-python@v5
        with:
          python-version: ${{ env.MAIN_PYTHON_VERSION }}
      - name: Download dist artifacts
        uses: actions/download-artifact@v4
        with:
          name: pyhanko-dist
          path: dist/
      - uses: ./.github/actions/test-job-setup
        with:
          dependency-group: testing-basic
      - name: Run smoke tests that should pass without optional dependencies
        # We run a couple of the "bread and butter" test modules, and the full CLI test suite except for
        # the PKCS#11 parts
        run: |
          python -m pytest \
            pyhanko_tests/test_signing.py pyhanko_tests/test_diff_analysis.py pyhanko_tests/test_crypt.py \
            pyhanko_tests/test_cms.py pyhanko_tests/cli_tests/*.py
  codecov-upload:
    permissions:
      actions: write
      contents: read
    runs-on: ubuntu-latest
    needs: [pytest-coverage,live-integration-tests]
    steps:
      # checkout necessary to ensure the uploaded report contains the correct paths
      - uses: actions/checkout@v4
      - name: Retrieve coverage reports
        uses: actions/download-artifact@v4
        with:
          pattern: coverage-*
          path: ./reports/
      - name: Upload all coverage reports to Codecov
        uses: codecov/codecov-action@v4
        with:
          token: ${{ secrets.CODECOV_TOKEN }}
          directory: ./reports/
          flags: unittests
          env_vars: OS,PYTHON
          name: codecov-umbrella
      - name: Clean up coverage reports
        continue-on-error: true
        uses: GeekyEggo/delete-artifact@v5
        with:
          name: coverage-*


Publish release

name: Publish release

on:
  workflow_dispatch:
    inputs:
      environment:
        type: environment
        description: "Environment in which to execute the release process"
  push:
    branches: [ "ci/*", "ci-*" ]
jobs:
  ci:
    name: Run CI pipeline
    uses: MatthiasValvekens/pyHanko/.github/workflows/build-pipeline.yml@master
    permissions:
      actions: write
      contents: read
    secrets:
      CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
  extract-params:
    name: Determine release parameters
    runs-on: ubuntu-latest
    permissions: {}
    outputs:
      publish-env: ${{ steps.setenv.outputs.envname }}
      release-version: ${{ steps.getversion.outputs.version }}
    steps:
      - id: setenv
        run: |
          if [[ $GITHUB_EVENT_NAME == 'release' ]]; then
              echo envname=release >> "$GITHUB_OUTPUT"
          elif [[ $GITHUB_EVENT_NAME == 'push' ]]; then
              # at times it may be convenient to temporarily turn on release-on-push
              # for testing purposes, so leaving this line in helps make that smoother
              echo envname=test-release >> "$GITHUB_OUTPUT"
          elif [[ $GITHUB_EVENT_NAME == 'workflow_dispatch' ]]; then
              echo "envname=${{ inputs.environment }}" >> "$GITHUB_OUTPUT"
          else
              echo "Cannot run release workflow for trigger event $GITHUB_EVENT_NAME"
              exit 1
          fi
          cat "$GITHUB_OUTPUT"
      - uses: actions/checkout@v4
      - name: Get version information
        id: getversion
        run: |
          set -eo pipefail
          grep __version__ < pyhanko/version.py \
            | sed "s/__version__ = '\(.*\)'/version=\1/" >> "$GITHUB_OUTPUT"
      - name: Generate release body
        run: |
          sed "s/:VERSION/$VERSION/g" < .github/gh-release-template.md > release.md
          cat release.md
        env:
          VERSION: ${{ steps.getversion.outputs.version }}
      - name: Upload release body
        uses: actions/upload-artifact@v4
        with:
          name: release-body
          path: release.md
  provenance:
    name: Generate SLSA provenance data
    needs: [ci]
    permissions:
      actions: read
      id-token: write
      contents: write  # https://github.com/slsa-framework/slsa-github-generator/issues/2044 :(
    uses: slsa-framework/slsa-github-generator/.github/workflows/generator_generic_slsa3.yml@v2.0.0
    with:
      base64-subjects: "${{ needs.ci.outputs.hashes }}"
      upload-assets: false
      provenance-name: multiple.intoto.jsonl
  publish:
    name: Publish release artifacts
    needs: [extract-params, provenance]
    runs-on: ubuntu-latest
    environment: ${{ needs.extract-params.outputs.publish-env }}
    permissions:
      # we use PyPI's trusted publisher model -> expose identity token
      id-token: write
      # we want to add sigstore's artifacts to the release on GitHub
      contents: write
      discussions: write
    steps:
    - name: Download dist artifacts
      uses: actions/download-artifact@v4
      with:
        name: pyhanko-dist
        path: dist/
    - name: Download provenance data
      uses: actions/download-artifact@v4
      with:
        name: multiple.intoto.jsonl
        path: provenance/
    - name: Download release body
      uses: actions/download-artifact@v4
      with:
        name: release-body
        path: release-body
    - name: Upload to PyPI
      uses: pypa/gh-action-pypi-publish@release/v1
      with:
        repository-url: ${{ vars.REPOSITORY_URL }}
    - name: Sign with sigstore
      uses: sigstore/gh-action-sigstore-python@v2.1.1
      with:
        inputs: ./dist/*
        # useful to inspect workflow artifacts in test runs
        upload-signing-artifacts: true
    - name: Create GitHub release
      if: needs.extract-params.outputs.publish-env == 'release' && startsWith(github.ref, 'refs/tags/')
      uses: softprops/action-gh-release@v2
      with:
        files: |
          dist/*.whl
          dist/*.tar.gz
          dist/*.sigstore
          provenance/multiple.intoto.jsonl
        body_path: release-body/release.md
        fail_on_unmatched_files: true
        discussion_category_name: Announcements
        prerelease: true
        name: pyHanko ${{ needs.extract-params.outputs.release-version }} beta


Static analysis

name: Static analysis

on: ["push", "pull_request"]

permissions:
  actions: read
  contents: read
jobs:
  lint:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-python@v5
      - run: pip install --upgrade pip isort black
      - name: Check import order
        run: isort --profile black --line-length 80 --check pyhanko pyhanko_tests
      - name: Run Black
        run: black -S --line-length 80 --check pyhanko pyhanko_tests
  mypy:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: jpetrucciani/mypy-check@0.991
        with:
          path: pyhanko
          requirements: "-e .[mypy] --config-settings editable_mode=strict"


Add issues to project

# A workflow for automatically adding all new issues to a project
name: Add issues to project
on:
  issues:
    types: [ opened ]
jobs:
  createCard:
    runs-on: ubuntu-latest
    steps:
      - name: Create project card
        uses: peter-evans/create-or-update-project-card@v1
        with:
          project-name: Otter-Grader Issues and Tasks
          column-name: Icebox


Publish release

name: Publish release

on:
  workflow_dispatch:
    inputs:
      version:
        description: 'New version number'
        required: true

jobs:
  build:
    if: contains('["chrispyles"]', github.actor)

    runs-on: ubuntu-latest

    env:
      TAR: /bin/tar

    steps:
      - name: Set up Python 3.8
        uses: actions/setup-python@v2
        with:
          python-version: 3.8

      - name: Only allow releases off of master (or temp for patches)
        run: |
          python3 -c 'import os, sys; sys.exit(os.environ["GITHUB_REF"] != "refs/heads/master" and os.environ["GITHUB_REF"] != "refs/heads/temp")'

      - uses: actions/checkout@v2

      - name: Install dependencies
        run: |
          python3 -m pip install -r requirements.txt twine wheel setuptools

      - name: Update versions
        run: |
          python3 release.py ${{ github.event.inputs.version }}

      - name: Build tutorial
        run: |
          make tutorial

      - name: Commit and push
        run: |
          git config --global user.name "github-actions"
          git config --global user.email "41898282+github-actions[bot]@users.noreply.github.com"
          git checkout -b release
          git commit -am 'release v${{ github.event.inputs.version }}'
          git push --set-upstream origin release

      - name: Create dist and push to PyPI
        env:
          TWINE_USERNAME: __token__
          TWINE_PASSWORD: ${{ secrets.PYPI_API_TOKEN }}
        run: |
          python3 setup.py sdist bdist_wheel
          python3 -m twine upload dist/*

      - name: Create a release on GitHub
        env:
          GITHUB_USER: ${{ secrets.GITHUB_USER }}
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
        run: |
            gh release create \
              --title 'v${{ github.event.inputs.version }}' \
              --target release \
              'v${{ github.event.inputs.version }}' \
              dist/*.tar.gz \
              dist/*.whl

      - name: Create a latest tag
        run: |
          git tag -f latest
          git push --force origin latest

      - name: PR release -> main
        env:
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
        run: |
          gh pr create \
            --repo ucbds-infra/otter-grader \
            --base master \
            --head ucbds-infra:release \
            --label "release" \
            --reviewer chrispyles \
            --milestone "v${{ github.event.inputs.version }}" \
            --title "Release v${{ github.event.inputs.version }}" \
            --body "Updates from the release of version ${{ github.event.inputs.version }}"


Run tests

# Workflow for running the test suite

name: Run tests

on:
  push:
    branches:
      - master
      - beta
      - release
  pull_request:
    branches:
      - master
      - beta
      - release
      - temp
  workflow_dispatch:

jobs:
  build:
    name: Test suite
    runs-on: ubuntu-latest

    env:
      TAR: /bin/tar
      DOCKER_BUILDKIT: 1
      DOCKER_CLI_EXPERIMENTAL: enabled

    defaults:
      run:
        shell: bash -l {0}

    steps:
      - uses: actions/checkout@v2

      - uses: docker/setup-buildx-action@v2
        with:
          install: true

      - uses: r-lib/actions/setup-tinytex@v2

      - uses: r-lib/actions/setup-pandoc@v2

      - uses: mamba-org/setup-micromamba@v1
        with:
          environment-file: environment.yml
          init-shell: >-
            bash
          cache-environment: true
          cache-environment-key: requirements-${{ hashFiles('requirements*.txt') }}

      - name: Install ottr
        run: |
          micromamba run -n otter-grader Rscript -e 'install.packages("ottr", dependencies=TRUE, repos="https://cran.r-project.org/")'

      - name: Run tests
        run: |
          micromamba activate otter-grader
          make testcov PYTESTOPTS="-vv" && coverage lcov -i

      - name: Coveralls
        uses: coverallsapp/github-action@v2


CI

name: CI

on:
  push:
    branches-ignore:
      - "dependabot/**"
  pull_request:

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

jobs:
  test:
    strategy:
      # Allows for matrix sub-jobs to fail without cancelling the rest
      fail-fast: false
      matrix:
        os: [ubuntu-latest, macos-latest, windows-latest]
        python-version: [3.8, 3.9, "3.10", 3.11, 3.12]

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

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

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

      - name: Test Installation
        shell: bash
        run: |
          pip install -r dev-requirements/build.txt

          pip install .
          pip uninstall -y hikari

          pip install .[speedups]
          pip uninstall -y hikari

      - name: Run tests
        shell: bash
        run: |
          pip install -r dev-requirements.txt
          nox -s pytest
          nox -s pytest-all-features -- --cov-append

          python scripts/ci/normalize_coverage.py

      - name: Upload coverage
        uses: actions/upload-artifact@v4
        with:
          name: .coverage.${{ matrix.os }}.${{ matrix.python-version }}
          path: .coverage
          retention-days: 1
          if-no-files-found: error

  upload-coverage:
    needs: [test]
    runs-on: ubuntu-latest

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

      - name: Setup python
        uses: actions/setup-python@v5
        with:
          python-version: "3.10"

      - name: Download coverage
        uses: actions/download-artifact@v4
        with:
          path: coverages/
          # Not specifying any name will lead to the download of all available artifacts

      # Since artifacts v2, we can no longer upload multiple artifacts
      # with the same name and have them re-download as if it were a directory.
      # For this reason, we need to download all available artifacts, filter
      # out for only coverage files and then place them in the root directory
      # with the name of the artifact
      - name: Extract individual coverage files
        run: |
          cd coverages

          for coverage_dir in ./.coverage.*; do
              mv "$coverage_dir/.coverage" "../$coverage_dir"
              rmdir "$coverage_dir"
          done

          cd ..

      - name: Combine coverage
        run: |
          pip install -r dev-requirements/coverage.txt
          coverage combine
          coverage xml
          coverage report

      - name: Upload coverage to codeclimate
        uses: paambaati/codeclimate-action@v8.0.0
        env:
          CC_TEST_REPORTER_ID: d40e64ea0ff74713f79365fea4378ab51a2141ad4fcf0fb118496bbf560d4192
        with:
          coverageLocations: .coverage.xml:coverage.py

  linting:
    runs-on: ubuntu-latest

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

      - name: Setup python
        uses: actions/setup-python@v5
        with:
          python-version: 3.8

      - name: Check stubs
        if: always()
        run: |
          pip install -r dev-requirements.txt
          nox -s generate-stubs
          if [ "$(git status --short)" ]; then
            echo "Stubs were not updated accordingly to the changes. Please run 'nox -s generate-stubs' and commit the changes to fix this."
            exit 1
          fi

      - name: Audit
        if: always()
        run: |
          nox -s audit

      - name: Mypy
        if: always()
        run: |
          nox -s mypy

      - name: Verify types
        if: always()
        run: |
          nox -s verify-types

      - name: Flake8
        if: always()
        run: |
          nox -s flake8

      - name: Slotscheck
        if: always()
        run: |
          nox -s slotscheck

      - name: Codespell
        if: always()
        run: |
          nox -s codespell

      - name: Check trailing whitespaces
        if: always()
        run: |
          nox -s check-trailing-whitespaces

  twemoji:
    runs-on: ubuntu-latest

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

      - name: Setup python
        uses: actions/setup-python@v5
        with:
          python-version: 3.8

      - name: Test twemoji mapping
        run: |
          pip install -r dev-requirements.txt
          nox -s twemoji-test

  docs:
    runs-on: ubuntu-latest

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

      - name: Setup python
        uses: actions/setup-python@v5
        with:
          # NOTE: This should be kept up to date with .readthedocs.yaml
          python-version: 3.11

      - name: Build documentation
        run: |
          pip install -r dev-requirements.txt
          nox -s mkdocs

      - name: Upload artifacts
        if: github.event_name == 'pull_request'
        uses: actions/upload-artifact@v4
        with:
          name: docs
          path: public/docs
          retention-days: 2
          if-no-files-found: error

  # Allows us to add this as a required check in Github branch rules, as all the other jobs are subject to change
  ci-done:
    needs: [upload-coverage, linting, twemoji, docs]
    if: always() && !cancelled()

    runs-on: ubuntu-latest

    steps:
      - name: Set status based on required jobs
        env:
          RESULTS: ${{ join(needs.*.result, ' ') }}
        run: |
          for result in $RESULTS; do
            if [ "$result" != "success" ]; then
              exit 1
            fi
          done


CodeQL

name: CodeQL

on:
  push:
    branches-ignore:
      - "dependabot/**"
  pull_request:
  schedule:
    - cron: "0 0 * * *" # Every day at 00:00

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

jobs:
  analyze:
    name: Analyze
    runs-on: ubuntu-latest

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

    - name: Initialize CodeQL
      uses: github/codeql-action/init@v3
      with:
        languages: python

    - name: Perform CodeQL Analysis
      uses: github/codeql-action/analyze@v3


Fragments check

name: Fragments check

on:
  pull_request:
    types: [labeled, unlabeled, opened, reopened, synchronize]
    branches:
      - master

jobs:
  check-fragment-added:
    if: github.event.pull_request.user.type != 'Bot' && !contains(github.event.pull_request.labels.*.name, 'skip-fragment-check')
    runs-on: ubuntu-latest

    steps:
      - name: Checkout repository
        uses: actions/checkout@v4
        with:
          # `towncrier check` runs `git diff --name-only origin/main...`, which
          # needs a non-shallow clone.
          fetch-depth: 0

      - name: Setup python
        uses: actions/setup-python@v5
        with:
          python-version: 3.8

      - name: Check if changelog fragment was added
        run: |
          pip install -r dev-requirements/towncrier.txt

          if ! towncrier check --compare-with origin/${{ github.base_ref }}; then
            exit 1
          fi

      - name: Check if linked to the correct pull request
        run: |
          diff=$(git diff origin/${{ github.base_ref }} HEAD --name-only)

          changelog_fragments=$(echo "$diff" | grep "^changes/")
          valid_changelog_fragments=$(echo "$diff" | grep "^changes/${{ github.event.number }}\.")

          if [ "$changelog_fragments" != "$valid_changelog_fragments" ]; then
            exit 1
          fi


Prepare Release

name: Prepare Release

on:
  workflow_dispatch:
    inputs:
      version:
        description: 'The version to prepare the release for'
        required: true

jobs:
  prepare-release:
    runs-on: ubuntu-latest

    if: github.ref == 'refs/heads/master'

    steps:
      - name: Generate token
        id: generate_token
        uses: tibdex/github-app-token@v2
        with:
          app_id: ${{ secrets.APP_ID }}
          private_key: ${{ secrets.PRIVATE_KEY }}

      - name: Checkout repository
        uses: actions/checkout@v4
        with:
          token: ${{ steps.generate_token.outputs.token }}

      - name: Setup git config
        run: |
          git config --global user.name "hikari-bot"
          git config --global user.email "90276125+hikari-bot[bot]@users.noreply.github.com"

      - name: Setup python
        uses: actions/setup-python@v5
        with:
          python-version: 3.8

      - name: Run prepare script
        env:
          VERSION: ${{ inputs.version }}
        run: bash scripts/ci/prepare-release.sh

      - name: Create pull request
        uses: repo-sync/pull-request@v2
        with:
          # We need to use a bot token to be able to trigger workflows that listen to pull_request calls
          github_token: ${{ steps.generate_token.outputs.token }}
          source_branch: task/prepare-release-${{ github.event.inputs.version }}
          destination_branch: master
          pr_assignee: ${{ github.event.sender.login }}
          pr_title: Prepare for release of ${{ github.event.inputs.version }}
          pr_body: |
            Release preparation triggered by @${{ github.event.sender.login }}.

            Once the pull request is merged, you can trigger a PyPI release by creating a GitHub release for \`${{ github.event.inputs.version }}\` in the repository.


Release

name: Release

on:
  release:
    types: [published]

jobs:
  release:
    runs-on: ubuntu-latest

    steps:
      - name: Generate token
        id: generate_token
        uses: tibdex/github-app-token@v2
        with:
          app_id: ${{ secrets.APP_ID }}
          private_key: ${{ secrets.PRIVATE_KEY }}

      - name: Checkout repository
        uses: actions/checkout@v4
        with:
          token: ${{ steps.generate_token.outputs.token }}

      - name: Setup git config
        run: |
          git config --global user.name "hikari-bot"
          git config --global user.email "90276125+hikari-bot[bot]@users.noreply.github.com"

      - name: Setup python
        uses: actions/setup-python@v5
        with:
          python-version: 3.8

      - name: Temporarily disable "include administrators" branch protection
        uses: benjefferies/branch-protection-bot@v1.1.2
        with:
          access_token: ${{ steps.generate_token.outputs.token }}
          enforce_admins: false

      - name: Release
        env:
          VERSION: ${{ github.event.release.tag_name }}
          DEPLOY_WEBHOOK_URL: ${{ secrets.DEPLOY_WEBHOOK_URL }}
          TWINE_USERNAME: ${{ secrets.TWINE_USERNAME }}
          TWINE_PASSWORD: ${{ secrets.TWINE_PASSWORD }}
        run: bash scripts/ci/release.sh

      - name: Re-enable "include administrators" branch protection
        uses: benjefferies/branch-protection-bot@v1.1.2
        if: always()
        with:
          access_token: ${{ steps.generate_token.outputs.token }}
          enforce_admins: true


name: documentation_dev
on:
  pull_request_target:
    branches:
      - main
    types: [closed]
  push:
    branches:
      - main

permissions:
  contents: write

jobs:
  docs:
    runs-on: ubuntu-latest
    steps:
      - name: Wait for version update
        run: |
          sleep 60
      - name: Checkout
        uses: actions/checkout@v4
      - uses: actions/setup-python@v4
        with:
          python-version: '3.10'

      - name: Install and configure Poetry
        uses: snok/install-poetry@v1

      - name: Install dependencies
        run: |
          curl -C - https://raw.githubusercontent.com/rlberry-py/rlberry/main/pyproject.toml > pyproject.toml
          poetry install --all-extras --with dev --sync
      - name: Sphinx build
        run: |
          poetry run sphinx-build docs _build
      - uses: actions/checkout@v4
        with:
          # This is necessary so that we have the tags.
          fetch-depth: 0
          ref: gh-pages
          path: gh_pages
      - name: copy stable and preview version changes
        run: |
          cp -rv  gh_pages/stable _build/stable || echo "Ignoring exit status"
          cp -rv  gh_pages/preview_pr _build/preview_pr || echo "Ignoring exit status"
      - name: Deploy to GitHub Pages
        uses: peaceiris/actions-gh-pages@v3
        if: ${{ github.event_name == 'push' && github.ref == 'refs/heads/main' }}
        with:
          publish_branch: gh-pages
          github_token: ${{ secrets.GITHUB_TOKEN }}
          publish_dir: _build/
          force_orphan: true


name: documentation_stable
on:
  push:
    # Pattern matched against refs/tags
    tags:
      - '*'           # Push events to every tag not containing /

permissions:
  contents: write

jobs:
  docs:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
        with:
          fetch-depth: 0
          fetch-tags: true
          path: main
      - name: checkout latest
        run: |
          cd main
          git checkout $(git describe --tags $(git rev-list --tags --max-count=1))
          cd ..
      - uses: actions/setup-python@v4
        with:
          python-version: '3.10'
      - name: Install and configure Poetry
        uses: snok/install-poetry@v1

      - name: Install dependencies
        run: |
          cd main
          poetry install --all-extras --with dev --sync
      - name: Sphinx build
        run: |
          poetry run sphinx-build docs ../_build
          cd ..
      - uses: actions/checkout@v4
        with:
          # This is necessary so that we have the tags.
          fetch-depth: 0
          ref: gh-pages
          path: gh_pages
      - name: Commit documentation changes
        run: |
          cd gh_pages
          rm -r stable || echo "Ignoring exit status"
          mkdir stable
          cp -rv  ../_build/* stable
          git config user.name github-actions
          git config user.email github-actions@github.com
          git add .
          git commit -m "Documentation Stable"
          git push


Version Control

name: Version Control
on:
  pull_request_target:
    branches:
      - main
    types: [closed]

jobs:
  versionjob:
    if: github.event.pull_request.merged == true
    runs-on: ubuntu-latest
    steps:
      - uses: actions/setup-python@v4
        with:
          python-version: '3.10'
      - uses: actions/checkout@v4
        with:
          # This is necessary so that we have the tags.
          fetch-depth: 0
          ref: main
      - uses: mtkennerly/dunamai-action@v1
        with:
          env-var: MY_VERSION
      - run: echo $MY_VERSION
      - uses: snok/install-poetry@v1
      - run: poetry version "v$MY_VERSION "
      - uses: EndBug/add-and-commit@v8
        with:
          add: 'pyproject.toml'
          default_author: github_actor
          message: 'Writing version with github action [skip ci]'


preview

name: "preview"

on:
  workflow_run:
    workflows: ["ready_for_review"]
    types:
      - completed

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

jobs:
  docs:
    runs-on: ubuntu-latest
    if: ${{ github.event.workflow_run.conclusion == 'success' }}
    steps:
      - name: Download workflow artifact
        uses: dawidd6/action-download-artifact@v2.11.0
        with:
          github_token: ${{ secrets.GITHUB_TOKEN }}
          workflow: peek_icons.yml
          run_id: ${{ github.event.workflow_run.id }}

      - name: Read the pr_num file
        id: pr_num_reader
        uses: juliangruber/read-file-action@v1.0.0
        with:
          path: ./pr_num/pr_num.txt

      - name: Checkout
        uses: actions/checkout@v4
        with:
          path: main
      - name: Checkout Pull Request
        env:
          GITHUB_USER: ${{ secrets.GITHUB_USER }}
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
        run: |
          cd main
          gh pr checkout ${{ steps.pr_num_reader.outputs.content }}
          cd ..

      - uses: actions/setup-python@v4
        with:
          python-version: '3.10'

      - name: Install and configure Poetry
        uses: snok/install-poetry@v1
      - name: Install dependencies and build doc
        run: |
          set +e
          cd main
          curl -C - https://raw.githubusercontent.com/rlberry-py/rlberry/main/pyproject.toml > pyproject.toml
          poetry install --all-extras --with dev --sync
          poetry run sphinx-build docs ../_build
          echo "exitcode=$?" >> $GITHUB_ENV
          cd ..

      - uses: actions/checkout@v4
        with:
          # This is necessary so that we have the tags.
          fetch-depth: 0
          ref: gh-pages
          path: gh_pages

      - name: Commit documentation changes
        if: ${{ env.exitcode == 0 }}
        run: |
          cd gh_pages
          rm -r preview_pr || echo "Ignoring exit status"
          mkdir preview_pr
          cp -rv  ../_build/* preview_pr
          git config user.name github-actions
          git config user.email github-actions@github.com
          git add .
          git commit -m "Preview documentation"
          git push
      - name: Comment on the PR about the result
        uses: jungwinter/comment@v1 # let us comment on a specific PR
        if:  ${{ env.exitcode == 0 }}
        env:
          MESSAGE: |
            Hello,
            The build of the doc succeeded. The documentation preview is available here:
            https://rlberry-py.github.io/rlberry/preview_pr

        with:
          type: create
          issue_number: ${{ steps.pr_num_reader.outputs.content }}
          token: ${{ secrets.GITHUB_TOKEN }}
          body: >
            ${{ format(env.MESSAGE,
            fromJSON(steps.icons_overview_img_step.outputs.markdown_urls)[0],
            join(fromJSON(steps.icons_detailed_img_step.outputs.markdown_urls), '')) }}

      - name: Comment on the PR about the result, fail
        uses: jungwinter/comment@v1 # let us comment on a specific PR
        if: ${{ env.exitcode != 0 }}
        env:
          MESSAGE: |
            Hello,
            The build of the doc failed. Look up the reason here:
            https://github.com/rlberry-py/rlberry/actions/workflows/preview.yml

        with:
          type: create
          issue_number: ${{ steps.pr_num_reader.outputs.content }}
          token: ${{ secrets.GITHUB_TOKEN }}
          body: >
            ${{ format(env.MESSAGE,
            fromJSON(steps.icons_overview_img_step.outputs.markdown_urls)[0],
            join(fromJSON(steps.icons_detailed_img_step.outputs.markdown_urls), '')) }}
      - name: Exit
        run: exit "$exitcode"


name: ready_for_review

on:
  pull_request:
    types: [labeled, opened, reopened, synchronize]

jobs:
  build:
    if: contains( github.event.pull_request.labels.*.name, 'ready for review')
    runs-on: ubuntu-latest

    steps:
    - uses: actions/checkout@v4
    - name: Run a one-line script
      run: echo ready for review!
    - name: Save the PR number in an artifact
      shell: bash
      env:
        PR_NUM: ${{ github.event.number }}
      run: echo $PR_NUM > pr_num.txt

    - name: Upload the PR number
      uses: actions/upload-artifact@v2
      with:
        name: pr_num
        path: ./pr_num.txt


Stale PRs

name: "Stale PRs"
on:
  schedule:
  # * is a special character in YAML so you have to quote this string
  - cron: "0 0 * * *"

permissions:
  contents: read

jobs:
  stale:
    permissions:
      pull-requests: write
    if: github.repository_owner == 'pandas-dev'
    runs-on: ubuntu-22.04
    steps:
    - uses: actions/stale@v9
      with:
        repo-token: ${{ secrets.GITHUB_TOKEN }}
        stale-pr-message: "This pull request is stale because it has been open for thirty days with no activity. Please [update](https://pandas.pydata.org/pandas-docs/stable/development/contributing.html#updating-your-pull-request) and respond to this comment if you're still interested in working on this."
        stale-pr-label: "Stale"
        exempt-pr-labels: "Needs Review,Blocked,Needs Discussion"
        days-before-issue-stale: -1
        days-before-pr-stale: 30
        days-before-close: -1
        remove-stale-when-updated: false
        debug-only: false


Code Checks

name: Code Checks

on:
  push:
    branches:
      - main
      - 2.2.x
  pull_request:
    branches:
      - main
      - 2.2.x

env:
  ENV_FILE: environment.yml
  PANDAS_CI: 1

permissions:
  contents: read

# pre-commit run by https://pre-commit.ci/
jobs:
  docstring_typing_manual_hooks:
    name: Docstring validation, typing, and other manual pre-commit hooks
    runs-on: ubuntu-22.04
    defaults:
      run:
        shell: bash -el {0}

    concurrency:
      # https://github.community/t/concurrecy-not-work-for-push/183068/7
      group: ${{ github.event_name == 'push' && github.run_number || github.ref }}-code-checks
      cancel-in-progress: true

    steps:
    - name: Checkout
      uses: actions/checkout@v4
      with:
        fetch-depth: 0

    - name: Set up Conda
      uses: ./.github/actions/setup-conda

    - name: Build Pandas
      id: build
      uses: ./.github/actions/build_pandas
      with:
        editable: false

    # The following checks are independent of each other and should still be run if one fails

    # TODO: The doctests have to be run first right now, since the Cython doctests only work
    # with pandas installed in non-editable mode
    # This can be removed once pytest-cython doesn't require C extensions to be installed inplace
    - name: Run doctests
      run: cd ci && ./code_checks.sh doctests
      if: ${{ steps.build.outcome == 'success' && always() }}

    - name: Install pandas in editable mode
      id: build-editable
      if: ${{ steps.build.outcome == 'success' && always() }}
      uses: ./.github/actions/build_pandas
      with:
        editable: true

    - name: Check for no warnings when building single-page docs
      run: ci/code_checks.sh single-docs
      if: ${{ steps.build.outcome == 'success' && always() }}

    - name: Run checks on imported code
      run: ci/code_checks.sh code
      if: ${{ steps.build.outcome == 'success' && always() }}

    - name: Run docstring validation
      run: ci/code_checks.sh docstrings
      if: ${{ steps.build.outcome == 'success' && always() }}

    - name: Run check of documentation notebooks
      run: ci/code_checks.sh notebooks
      if: ${{ steps.build.outcome == 'success' && always() }}

    - name: Use existing environment for type checking
      run: |
        echo $PATH >> $GITHUB_PATH
        echo "PYTHONHOME=$PYTHONHOME" >> $GITHUB_ENV
        echo "PYTHONPATH=$PYTHONPATH" >> $GITHUB_ENV
      if: ${{ steps.build.outcome == 'success' && always() }}

    - name: Typing
      uses: pre-commit/action@v3.0.1
      with:
        extra_args: --verbose --hook-stage manual --all-files
      if: ${{ steps.build.outcome == 'success' && always() }}

    - name: Run docstring validation script tests
      run: pytest scripts
      if: ${{ steps.build.outcome == 'success' && always() }}

  asv-benchmarks:
    name: ASV Benchmarks
    runs-on: ubuntu-22.04
    defaults:
      run:
        shell: bash -el {0}

    concurrency:
      # https://github.community/t/concurrecy-not-work-for-push/183068/7
      group: ${{ github.event_name == 'push' && github.run_number || github.ref }}-asv-benchmarks
      cancel-in-progress: true

    steps:
    - name: Checkout
      uses: actions/checkout@v4
      with:
        fetch-depth: 0

    - name: Set up Conda
      uses: ./.github/actions/setup-conda

    - name: Build Pandas
      id: build
      uses: ./.github/actions/build_pandas

    - name: Run ASV benchmarks
      run: |
        cd asv_bench
        asv machine --yes
        asv run --quick --dry-run --durations=30 --python=same --show-stderr

  build_docker_dev_environment:
    name: Build Docker Dev Environment
    runs-on: ubuntu-22.04
    defaults:
      run:
        shell: bash -el {0}

    concurrency:
      # https://github.community/t/concurrecy-not-work-for-push/183068/7
      group: ${{ github.event_name == 'push' && github.run_number || github.ref }}-build_docker_dev_environment
      cancel-in-progress: true

    steps:
      - name: Clean up dangling images
        run: docker image prune -f

      - name: Checkout
        uses: actions/checkout@v4
        with:
          fetch-depth: 0

      - name: Build image
        run: docker build --pull --no-cache --tag pandas-dev-env .

      - name: Show environment
        run: docker run --rm pandas-dev-env python -c "import pandas as pd; print(pd.show_versions())"

  requirements-dev-text-installable:
    name: Test install requirements-dev.txt
    runs-on: ubuntu-22.04

    concurrency:
      # https://github.community/t/concurrecy-not-work-for-push/183068/7
      group: ${{ github.event_name == 'push' && github.run_number || github.ref }}-requirements-dev-text-installable
      cancel-in-progress: true

    steps:
      - name: Checkout
        uses: actions/checkout@v4
        with:
          fetch-depth: 0

      - name: Setup Python
        id: setup_python
        uses: actions/setup-python@v5
        with:
          python-version: '3.10'
          cache: 'pip'
          cache-dependency-path: 'requirements-dev.txt'

      - name: Install requirements-dev.txt
        run: pip install -r requirements-dev.txt

      - name: Check Pip Cache Hit
        run: echo ${{ steps.setup_python.outputs.cache-hit }}


Doc Build and Upload

name: Doc Build and Upload

on:
  push:
    branches:
      - main
      - 2.2.x
    tags:
      - '*'
  pull_request:
    branches:
      - main
      - 2.2.x

env:
  ENV_FILE: environment.yml
  PANDAS_CI: 1
  GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

permissions:
  contents: read

jobs:
  web_and_docs:
    name: Doc Build and Upload
    runs-on: ubuntu-22.04

    concurrency:
      # https://github.community/t/concurrecy-not-work-for-push/183068/7
      group: ${{ github.event_name == 'push' && github.run_number || github.ref }}-web-docs
      cancel-in-progress: true

    defaults:
      run:
        shell: bash -el {0}

    steps:
    - name: Checkout
      uses: actions/checkout@v4
      with:
        fetch-depth: 0

    - name: Set up Conda
      uses: ./.github/actions/setup-conda

    - name: Build Pandas
      uses: ./.github/actions/build_pandas

    - name: Test website
      run: python -m pytest web/

    - name: Build website
      run: python web/pandas_web.py web/pandas --target-path=web/build

    - name: Build documentation
      run: doc/make.py --warnings-are-errors

    - name: Build documentation zip
      run: doc/make.py zip_html

    - name: Install ssh key
      run: |
        mkdir -m 700 -p ~/.ssh
        echo "${{ secrets.server_ssh_key }}" > ~/.ssh/id_rsa
        chmod 600 ~/.ssh/id_rsa
        echo "${{ secrets.server_ip }} ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBFjYkJBk7sos+r7yATODogQc3jUdW1aascGpyOD4bohj8dWjzwLJv/OJ/fyOQ5lmj81WKDk67tGtqNJYGL9acII=" > ~/.ssh/known_hosts
      if: github.event_name == 'push' && (github.ref == 'refs/heads/main' || startsWith(github.ref, 'refs/tags/'))

    - name: Copy cheatsheets into site directory
      run: cp doc/cheatsheet/Pandas_Cheat_Sheet* web/build/

    - name: Upload web
      run: rsync -az --delete --exclude='pandas-docs' --exclude='docs' --exclude='benchmarks' web/build/ web@${{ secrets.server_ip }}:/var/www/html
      if: github.event_name == 'push' && github.ref == 'refs/heads/main'

    - name: Upload dev docs
      run: rsync -az --delete doc/build/html/ web@${{ secrets.server_ip }}:/var/www/html/pandas-docs/dev
      if: github.event_name == 'push' && github.ref == 'refs/heads/main'

    - name: Upload prod docs
      run: rsync -az --delete doc/build/html/ web@${{ secrets.server_ip }}:/var/www/html/pandas-docs/version/${GITHUB_REF_NAME:1}
      if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags/')

    - name: Move docs into site directory
      run: mv doc/build/html web/build/docs

    - name: Save website as an artifact
      uses: actions/upload-artifact@v4
      with:
        name: website
        path: web/build
        retention-days: 14


CodeQL

name: CodeQL
on:
  schedule:
    # every day at midnight
    - cron: "0 0 * * *"

concurrency:
  group: ${{ github.repository }}-${{ github.head_ref || github.sha }}-${{ github.workflow }}
  cancel-in-progress: true

permissions:
  contents: read

jobs:
  analyze:
    runs-on: ubuntu-22.04
    permissions:
      actions: read
      contents: read
      security-events: write
    if: github.repository_owner == 'pandas-dev'

    strategy:
      fail-fast: false
      matrix:
        language:
          - python

    steps:
      - uses: actions/checkout@v4
      - uses: github/codeql-action/init@v3
        with:
          languages: ${{ matrix.language }}
      - uses: github/codeql-action/autobuild@v3
      - uses: github/codeql-action/analyze@v3


Wheel builder

# Workflow to build wheels for upload to PyPI.
# Inspired by numpy's cibuildwheel config https://github.com/numpy/numpy/blob/main/.github/workflows/wheels.yml
#
# In an attempt to save CI resources, wheel builds do
# not run on each push but only weekly and for releases.
# Wheel builds can be triggered from the Actions page
# (if you have the permissions) on a commit to main.
#
# Alternatively, you can add labels to the pull request in order to trigger wheel
# builds.
# The label(s) that trigger builds are:
# - Build
name: Wheel builder

on:
  schedule:
  # 3:27 UTC every day
  - cron: "27 3 * * *"
  push:
  pull_request:
    types: [labeled, opened, synchronize, reopened]
    paths-ignore:
      - "doc/**"
      - "web/**"
  workflow_dispatch:

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

permissions:
  contents: read

jobs:
  build_sdist:
    name: Build sdist
    if: >-
      (github.event_name == 'schedule') ||
      github.event_name == 'workflow_dispatch' ||
      (github.event_name == 'pull_request' &&
      contains(github.event.pull_request.labels.*.name, 'Build')) ||
      (github.event_name == 'push' && startsWith(github.ref, 'refs/tags/v') && ( ! endsWith(github.ref, 'dev0')))
    runs-on: ubuntu-22.04
    env:
      IS_PUSH: ${{ github.event_name == 'push' && startsWith(github.ref, 'refs/tags/v') }}
      IS_SCHEDULE_DISPATCH: ${{ github.event_name == 'schedule' || github.event_name == 'workflow_dispatch' }}
    outputs:
      sdist_file: ${{ steps.save-path.outputs.sdist_name }}
    steps:
      - name: Checkout pandas
        uses: actions/checkout@v4
        with:
          fetch-depth: 0

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

      - name: Build sdist
        run: |
          python -m pip install build
          python -m build --sdist

      - uses: actions/upload-artifact@v4
        with:
          name: sdist
          path: ./dist/*

      - name: Sanity check sdist files
        run: |
          ls ./dist

      - name: Output sdist name
        id: save-path
        shell: bash -el {0}
        run: echo "sdist_name=$(ls ./dist)" >> "$GITHUB_OUTPUT"

  build_wheels:
    needs: build_sdist
    name: Build wheel for ${{ matrix.python[0] }}-${{ matrix.buildplat[1] }}
    if: >-
      (github.event_name == 'schedule') ||
      github.event_name == 'workflow_dispatch' ||
      (github.event_name == 'pull_request' &&
      contains(github.event.pull_request.labels.*.name, 'Build')) ||
      (github.event_name == 'push' && startsWith(github.ref, 'refs/tags/v') && ( ! endsWith(github.ref, 'dev0')))
    runs-on: ${{ matrix.buildplat[0] }}
    strategy:
      fail-fast: false
      matrix:
        # GitHub Actions doesn't support pairing matrix values together, let's improvise
        # https://github.com/github/feedback/discussions/7835#discussioncomment-1769026
        buildplat:
        - [ubuntu-22.04, manylinux_x86_64]
        - [ubuntu-22.04, musllinux_x86_64]
        - [macos-12, macosx_x86_64]
        # Note: M1 images on Github Actions start from macOS 14
        - [macos-14, macosx_arm64]
        - [windows-2022, win_amd64]
        # TODO: support PyPy?
        python: [["cp310", "3.10"], ["cp311", "3.11"], ["cp312", "3.12"]]
    env:
      IS_PUSH: ${{ github.event_name == 'push' && startsWith(github.ref, 'refs/tags/v') }}
      IS_SCHEDULE_DISPATCH: ${{ github.event_name == 'schedule' || github.event_name == 'workflow_dispatch' }}
    steps:
      - name: Checkout pandas
        uses: actions/checkout@v4
        with:
          fetch-depth: 0

      # TODO: Build wheels from sdist again
      # There's some sort of weird race condition?
      # within Github that makes the sdist be missing files

      # We need to build wheels from the sdist since the sdist
      # removes unnecessary files from the release
      - name: Download sdist (not macOS)
        #if: ${{ matrix.buildplat[1] != 'macosx_*' }}
        uses: actions/download-artifact@v4
        with:
          name: sdist
          path: ./dist

      - name: Output sdist name (macOS)
        id: save-path
        shell: bash -el {0}
        run: echo "sdist_name=$(ls ./dist)" >> "$GITHUB_ENV"

      # Python version used to build sdist doesn't matter
      # wheel will be built from sdist with the correct version
      - name: Unzip sdist (macOS)
        if: ${{ startsWith(matrix.buildplat[1], 'macosx') }}
        run: |
          tar -xzf ./dist/${{ env.sdist_name }} -C ./dist

      - name: Output sdist name (macOS)
        id: save-path2
        shell: bash -el {0}
        run: echo "sdist_name=$(cd ./dist && ls -d */)" >> "$GITHUB_ENV"

      - name: Build wheels
        uses: pypa/cibuildwheel@v2.19.1
        with:
         package-dir: ./dist/${{ startsWith(matrix.buildplat[1], 'macosx') && env.sdist_name || needs.build_sdist.outputs.sdist_file }}
        env:
          CIBW_PRERELEASE_PYTHONS: True
          CIBW_BUILD: ${{ matrix.python[0] }}-${{ matrix.buildplat[1] }}

      - name: Set up Python
        uses: mamba-org/setup-micromamba@v1
        with:
          environment-name: wheel-env
          # Use a fixed Python, since we might have an unreleased Python not
          # yet present on conda-forge
          create-args: >-
            python=3.11
            anaconda-client
            wheel
          cache-downloads: true
          cache-environment: true

      - name: Validate wheel RECORD
        shell: bash -el {0}
        run: for whl in $(ls wheelhouse); do wheel unpack wheelhouse/$whl -d /tmp; done

      # Testing on windowsservercore instead of GHA runner to fail on missing DLLs
      - name: Test Windows Wheels
        if: ${{ matrix.buildplat[1] == 'win_amd64' }}
        shell: pwsh
        run: |
          $TST_CMD = @"
          python -m pip install hypothesis>=6.46.1 pytest>=7.3.2 pytest-xdist>=2.2.0;
          python -m pip install `$(Get-Item pandas\wheelhouse\*.whl);
          python -c `'import pandas as pd; pd.test(extra_args=[`\"--no-strict-data-files`\", `\"-m not clipboard and not single_cpu and not slow and not network and not db`\"])`';
          "@
          # add rc to the end of the image name if the Python version is unreleased
          docker pull python:${{ matrix.python[1] == '3.12' && '3.12-rc' || format('{0}-windowsservercore', matrix.python[1]) }}
          docker run --env PANDAS_CI='1' -v ${PWD}:C:\pandas python:${{ matrix.python[1] == '3.12' && '3.12-rc' || format('{0}-windowsservercore', matrix.python[1]) }} powershell -Command $TST_CMD

      - uses: actions/upload-artifact@v4
        with:
          name: ${{ matrix.python[0] }}-${{ matrix.buildplat[1] }}
          path: ./wheelhouse/*.whl

      - name: Upload wheels & sdist
        if: ${{ success() && (env.IS_SCHEDULE_DISPATCH == 'true' || env.IS_PUSH == 'true') }}
        shell: bash -el {0}
        env:
          PANDAS_STAGING_UPLOAD_TOKEN: ${{ secrets.PANDAS_STAGING_UPLOAD_TOKEN }}
          PANDAS_NIGHTLY_UPLOAD_TOKEN: ${{ secrets.PANDAS_NIGHTLY_UPLOAD_TOKEN }}
        # trigger an upload to
        # https://anaconda.org/scientific-python-nightly-wheels/pandas
        # for cron jobs or "Run workflow" (restricted to main branch).
        # Tags will upload to
        # https://anaconda.org/multibuild-wheels-staging/pandas
        # The tokens were originally generated at anaconda.org
        run: |
          source ci/upload_wheels.sh
          set_upload_vars
          upload_wheels