name: CI

on:
  push:
    branches:
      - '**'
      - '!dependabot/**'
      - '!*-patch-*'
  pull_request:
  merge_group:
  workflow_dispatch:

permissions:
  contents: read

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

jobs:
  tox:
    strategy:
      fail-fast: false
      matrix:
        env: [lint, filename_matching, mypy, individual_coverage, old-dependencies, test-offline]
    name: ${{ matrix.env }}
    runs-on: ubuntu-latest
    steps:
      - uses: mhils/workflows/checkout@8fe88b311a66c441e01edfebe4cd90d8a47fa335
      - uses: mhils/workflows/setup-uv@8fe88b311a66c441e01edfebe4cd90d8a47fa335
      - uses: mhils/workflows/uv-sync@8fe88b311a66c441e01edfebe4cd90d8a47fa335
        with:
          args: --only-group tox

      - run: tox -e ${{ matrix.env }}

  test:
    strategy:
      fail-fast: false
      matrix:
        include:
          - os: ubuntu-latest
            py: "3.14"
          - os: windows-latest
            py: "3.14"
          - os: macos-latest
            py: "3.14"
          - os: ubuntu-latest
            py: "3.13"
          - os: ubuntu-latest
            py: "3.12"
    runs-on: ${{ matrix.os }}
    steps:
      - uses: mhils/workflows/checkout@8fe88b311a66c441e01edfebe4cd90d8a47fa335
      - uses: mhils/workflows/setup-uv@8fe88b311a66c441e01edfebe4cd90d8a47fa335
      - uses: mhils/workflows/uv-sync@8fe88b311a66c441e01edfebe4cd90d8a47fa335
        with:
          args: --only-group tox

      - run: tox -e py${{ matrix.py }}
      - uses: codecov/codecov-action@v6
        with:
          token: ${{ secrets.CODECOV_TOKEN }}
          files: ./coverage.xml

  build:
    strategy:
      fail-fast: false
      matrix:
        include:
          - image: macos-15
            platform: macos-arm64
          - image: macos-15-intel
            platform: macos-x86_64
          - image: windows-2025
            platform: windows
          - image: ubuntu-latest
            platform: linux-x86_64
            container: debian:11  # Old version so we get oldest glibc possible.
          - image: ubuntu-24.04-arm
            platform: linux-arm64
            container: debian:11  # Old version so we get oldest glibc possible.
    runs-on: ${{ matrix.image }}
    container:
      image: ${{ matrix.container }}
    steps:
      - if: matrix.container
        run: |
          apt-get update
          apt-get install --no-install-recommends -y git ca-certificates binutils build-essential
      - uses: mhils/workflows/checkout@8fe88b311a66c441e01edfebe4cd90d8a47fa335
      - uses: mhils/workflows/setup-uv@8fe88b311a66c441e01edfebe4cd90d8a47fa335
      - uses: mhils/workflows/uv-sync@8fe88b311a66c441e01edfebe4cd90d8a47fa335
      - run: uv pip install .  # pyinstaller 5.9 does not like pyproject.toml + editable installs.

      - if: runner.os == 'macOS' && github.repository == 'mitmproxy/mitmproxy'
            && (startsWith(github.ref, 'refs/heads/') || startsWith(github.ref, 'refs/tags/'))
        id: keychain
        uses: apple-actions/import-codesign-certs@fe74d46e82474f87e1ba79832ad28a4013d0e33a
        with:
          keychain: ${{ runner.temp }}/temp
          p12-file-base64: ${{ secrets.APPLE_CERTIFICATE }}
          p12-password: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }}
      - if: runner.os == 'macOS' && github.repository == 'mitmproxy/mitmproxy'
            && (startsWith(github.ref, 'refs/heads/') || startsWith(github.ref, 'refs/tags/'))
        run: |
          python -u release/build.py macos-app \
            --keychain "${{ runner.temp }}/temp.keychain" \
            --team-id "S8XHQB96PW" \
            --apple-id "${{ secrets.APPLE_ID }}" \
            --password "${{ secrets.APPLE_APP_PASSWORD }}"

      # Linux
      - if: runner.os == 'Linux'
        run: python -u release/build.py standalone-binaries

      # Windows
      - if: runner.os == 'Windows'
        run: python -u release/build.py standalone-binaries

      - uses: actions/upload-artifact@v7
        with:
          name: binaries.${{ matrix.platform }}
          path: release/dist

  build-wheel:
    uses: mhils/workflows/.github/workflows/python-build.yml@8fe88b311a66c441e01edfebe4cd90d8a47fa335
    with:
      artifact: binaries.wheel

  build-windows-installer:
    runs-on: windows-latest
    if: github.repository == 'mitmproxy/mitmproxy' && (
        github.ref == 'refs/heads/main' ||
        github.ref == 'refs/heads/citest' ||
        startsWith(github.ref, 'refs/tags/')
      )
    steps:
      - uses: mhils/workflows/checkout@8fe88b311a66c441e01edfebe4cd90d8a47fa335
      - uses: mhils/workflows/setup-uv@8fe88b311a66c441e01edfebe4cd90d8a47fa335
      - uses: mhils/workflows/uv-sync@8fe88b311a66c441e01edfebe4cd90d8a47fa335
      - run: uv pip install .  # pyinstaller 5.9 does not like pyproject.toml + editable installs.
      - run: python -u release/build.py installbuilder-installer msix-installer
        env:
          CI_BUILD_KEY: ${{ secrets.CI_BUILD_KEY }}

      - uses: actions/upload-artifact@v7
        with:
          name: binaries.windows-installer
          path: release/dist

  test-web-ui:
    runs-on: ubuntu-latest
    steps:
      - uses: mhils/workflows/checkout@8fe88b311a66c441e01edfebe4cd90d8a47fa335
      - uses: actions/setup-node@v6.3.0
        with:
          node-version-file: .github/node-version.txt
      - name: Cache Node.js modules
        uses: actions/cache@v5.0.4
        with:
          # npm cache files are stored in `~/.npm` on Linux/macOS
          path: ~/.npm
          key: ${{ runner.OS }}-node-${{ hashFiles('**/package-lock.json') }}
          restore-keys: |
            ${{ runner.OS }}-node-
            ${{ runner.OS }}-
      - working-directory: ./web
        run: npm ci
      - working-directory: ./web
        run: npm test
      - uses: codecov/codecov-action@v6
        with:
          token: ${{ secrets.CODECOV_TOKEN }}
          files: ./web/coverage/coverage-final.json

  test-docker:
    runs-on: ubuntu-latest
    needs: build-wheel
    steps:
      - uses: mhils/workflows/checkout@8fe88b311a66c441e01edfebe4cd90d8a47fa335
      - uses: actions/download-artifact@v8
        with:
          name: binaries.wheel
          path: release/docker
      - name: Build container
        run: docker build --tag localtesting release/docker
      - name: Test container
        run: docker run --rm -v $PWD/release:/release localtesting mitmdump -s /release/selftest.py

  docs:
    runs-on: ubuntu-latest
    steps:
      - uses: mhils/workflows/checkout@8fe88b311a66c441e01edfebe4cd90d8a47fa335
      - uses: mhils/workflows/setup-uv@8fe88b311a66c441e01edfebe4cd90d8a47fa335
      - uses: mhils/workflows/uv-sync@8fe88b311a66c441e01edfebe4cd90d8a47fa335
      - run: |
          wget -q https://github.com/gohugoio/hugo/releases/download/v0.139.3/hugo_extended_0.139.3_linux-amd64.deb
          echo "3e58800d1fee57269208d07d104ae1a6ab886615344099f2dca0c6ad5279bc11 hugo_extended_0.139.3_linux-amd64.deb" | sha256sum -c
          sudo dpkg -i hugo*.deb

      - run: ./docs/build.py
      - uses: actions/upload-artifact@v7
        with:
          name: docs
          path: docs/public
      # For releases, also build the archive version of the docs.
      - run: ./docs/build.py
        env:
          DOCS_ARCHIVE: ${{ github.ref_name }}
      - if: startsWith(github.ref, 'refs/tags/')
        uses: actions/upload-artifact@v7
        with:
          name: docs-archive
          path: docs/public

  check:
    if: always()
    needs:
    - tox
    - test
    - test-docker
    - test-web-ui
    - build
    - build-wheel
    - build-windows-installer
    - docs
    uses: mhils/workflows/.github/workflows/alls-green.yml@8fe88b311a66c441e01edfebe4cd90d8a47fa335
    with:
      jobs: ${{ toJSON(needs) }}
      allowed-skips: build-windows-installer

  # Separate from everything else because slow.
  deploy-docker:
    if: github.repository == 'mitmproxy/mitmproxy' && (
        github.ref == 'refs/heads/main' ||
        github.ref == 'refs/heads/citest' ||
        startsWith(github.ref, 'refs/tags/')
      )
    permissions:
      id-token: write
      attestations: write
      packages: write
    environment: deploy-docker
    needs: check
    runs-on: ubuntu-latest
    steps:
      - uses: mhils/workflows/checkout@8fe88b311a66c441e01edfebe4cd90d8a47fa335
      - uses: actions/download-artifact@v8
        with:
          name: binaries.wheel
          path: release/docker
      - uses: docker/setup-qemu-action@ce360397dd3f832beb865e1373c09c0e9f86d70a # v4.0.0
      - uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd # v1.6.0

      - name: Login to Docker Hub
        uses: docker/login-action@b45d80f862d83dbcd57f89517bcf500b2ab88fb2 # v4.0.0
        with:
          username: mitmbot
          password: ${{ secrets.DOCKER_PASSWORD }}
      - name: Login to GitHub Container Registry
        uses: docker/login-action@b45d80f862d83dbcd57f89517bcf500b2ab88fb2 # v4.0.0
        with:
          registry: ghcr.io
          username: ${{ github.repository_owner }}
          password: ${{ secrets.GITHUB_TOKEN }}

      - name: Docker meta
        id: meta
        uses: docker/metadata-action@030e881283bb7a6894de51c315a6bfe6a94e05cf
        env:
          DOCKER_METADATA_ANNOTATIONS_LEVELS: index
        with:
          images: |
            mitmproxy/mitmproxy
            ghcr.io/mitmproxy/mitmproxy
          tags: |
            type=semver,pattern={{version}}
            type=semver,pattern={{major}}.{{minor}}
            type=semver,pattern={{major}}
            type=raw,value=dev,enable=${{ github.ref == 'refs/heads/main' }}
            type=raw,value=citest,enable=${{ github.ref == 'refs/heads/citest' }}

      - name: Build and push
        id: push
        uses: docker/build-push-action@d08e5c354a6adb9ed34480a06d141179aa583294 # v7.0.0
        with:
          context: release/docker
          platforms: linux/amd64,linux/arm64
          push: true
          provenance: false
          tags: ${{ steps.meta.outputs.tags }}
          labels: ${{ steps.meta.outputs.labels }}
          annotations: ${{ steps.meta.outputs.annotations }}
      - uses: actions/attest-build-provenance@v4
        with:
          subject-name: ghcr.io/${{ github.repository }}
          subject-digest: ${{ steps.push.outputs.digest }}

  deploy:
    # This action has access to our AWS keys, so we are extra careful here.
    # In particular, we don't blindly `pip install` anything to minimize the risk of supply chain attacks.
    if: github.repository == 'mitmproxy/mitmproxy' && (startsWith(github.ref, 'refs/heads/') || startsWith(github.ref, 'refs/tags/'))
    environment: ${{ (github.ref == 'refs/heads/citest' || startsWith(github.ref, 'refs/tags/')) && 'deploy-release' || 'deploy-snapshot' }}
    needs: check
    runs-on: ubuntu-latest
    permissions:
      id-token: write
      attestations: write
    env:
      # PyPI and MSFT keys are only available for the deploy-release environment
      # The AWS access key for snapshots is scoped to branches/* as well.
      TWINE_USERNAME: __token__
      TWINE_PASSWORD: ${{ secrets.TWINE_PASSWORD }}
      AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
      AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
      AWS_DEFAULT_REGION: us-west-2
      MSFT_APP_ID: 9NWNDLQMNZD7
      MSFT_TENANT_ID: ${{ secrets.MSFT_TENANT_ID }}
      MSFT_CLIENT_ID: ${{ secrets.MSFT_CLIENT_ID }}
      MSFT_CLIENT_SECRET: ${{ secrets.MSFT_CLIENT_SECRET }}
      R2_ACCOUNT_ID: ${{ secrets.R2_ACCOUNT_ID }}
      R2_ACCESS_KEY_ID: ${{ secrets.R2_ACCESS_KEY_ID }}
      R2_SECRET_ACCESS_KEY: ${{ secrets.R2_SECRET_ACCESS_KEY }}
    steps:
      - uses: mhils/workflows/checkout@8fe88b311a66c441e01edfebe4cd90d8a47fa335
      - uses: mhils/workflows/setup-uv@8fe88b311a66c441e01edfebe4cd90d8a47fa335
      - uses: mhils/workflows/uv-sync@8fe88b311a66c441e01edfebe4cd90d8a47fa335
        with:
          args: --only-group deploy

      - uses: actions/download-artifact@v8
        with:
          name: docs
          path: docs/public
      - if: startsWith(github.ref, 'refs/tags/')
        uses: actions/download-artifact@v8
        with:
          name: docs-archive
          path: docs/archive
      - uses: actions/download-artifact@v8
        with:
          pattern: binaries.*
          merge-multiple: true
          path: release/dist
      - id: provenance
        uses: actions/attest-build-provenance@v4
        with:
          subject-path: 'release/dist/*'
      - run: |
          REF=${{ github.ref_name }}
          mv ${{ steps.provenance.outputs.bundle-path }} release/dist/mitmproxy-${REF#v}.sigstore
      - run: ls docs/public
      - run: ls release/dist

      - run: ./release/deploy.py

      - name: Deploy to Microsoft Store (test flight)
        if: github.ref == 'refs/heads/citest'
        run: ./release/deploy-microsoft-store.py release/dist/*.msix
        env:
          MSFT_APP_FLIGHT: 174ca570-8cae-4444-9858-c07293f1f13a
      - name: Deploy to Microsoft Store
        if: startsWith(github.ref, 'refs/tags/')
        run: ./release/deploy-microsoft-store.py release/dist/*.msix
