---
name: CI for docker images

# Only run on PRs that touch the docker image, its build inputs, or any code
# whose toolchain the compile smoke test exercises (core + target platforms).

on:
  pull_request:
    paths:
      # Docker image and its build inputs.
      - "docker/**"
      - ".github/workflows/ci-docker.yml"
      - "requirements*.txt"
      - "pyproject.toml"
      - "platformio.ini"
      - "esphome/idf_component.yml"
      - "script/platformio_install_deps.py"
      # Core, build pipeline, toolchain, and target-platform changes can change
      # how a toolchain is set up or built, so re-run the per-toolchain compile
      # smoke test when they change.
      - "esphome/core/**"
      - "esphome/writer.py"
      - "esphome/build_gen/**"
      - "esphome/espidf/**"
      - "esphome/platformio/**"
      - "esphome/components/bk72xx/**"
      - "esphome/components/esp32/**"
      - "esphome/components/esp8266/**"
      - "esphome/components/host/**"
      - "esphome/components/libretiny/**"
      - "esphome/components/ln882x/**"
      - "esphome/components/nrf52/**"
      - "esphome/components/rp2040/**"
      - "esphome/components/rtl87xx/**"
      - "esphome/components/zephyr/**"

permissions:
  contents: read  # actions/checkout only

concurrency:
  # yamllint disable-line rule:line-length
  group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
  cancel-in-progress: true

jobs:
  check-docker:
    name: Build docker containers
    runs-on: ${{ matrix.os }}
    permissions:
      contents: read   # actions/checkout to load Dockerfile and build context
      packages: write  # push branch-tagged images to ghcr.io for local testing
    strategy:
      fail-fast: false
      matrix:
        os: ["ubuntu-24.04", "ubuntu-24.04-arm"]
        build_type:
          - "ha-addon"
          - "docker"
          # - "lint"
    outputs:
      tag: ${{ steps.tag.outputs.tag }}
      push: ${{ steps.tag.outputs.push }}
    steps:
      - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
      - name: Set up Python
        uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0
        with:
          python-version: "3.12"
      - name: Set up Docker Buildx
        uses: docker/setup-buildx-action@bb05f3f5519dd87d3ba754cc423b652a5edd6d2c # v4.2.0

      - name: Determine tag and whether to push
        id: tag
        env:
          HEAD_REF: ${{ github.head_ref || github.ref_name }}
        run: |
          # Sanitize the branch name into a valid docker tag: replace invalid
          # characters, ensure the first character is valid (tags must start
          # with [A-Za-z0-9_]), and cap the length at 128 characters.
          branch="$HEAD_REF"
          tag="${branch//[^a-zA-Z0-9_.-]/-}"
          case "$tag" in
            [a-zA-Z0-9_]*) ;;
            *) tag="pr-${tag}" ;;
          esac
          tag="${tag:0:128}"
          echo "tag=${tag}" >> "$GITHUB_OUTPUT"
          # Only push branch images for same-repo pull requests. Push events
          # only fire for dev/beta/release, whose images are owned by the
          # release pipeline -- never overwrite those from here.
          if [ "${{ github.event_name }}" = "pull_request" ] \
            && [ "${{ github.repository }}" = "esphome/esphome" ] \
            && [ "${{ github.event.pull_request.head.repo.full_name }}" = "esphome/esphome" ]; then
            echo "push=true" >> "$GITHUB_OUTPUT"
          else
            echo "push=false" >> "$GITHUB_OUTPUT"
          fi

      - name: Log in to the GitHub container registry
        if: steps.tag.outputs.push == 'true'
        uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f # v4.6.0
        with:
          registry: ghcr.io
          username: ${{ github.actor }}
          password: ${{ secrets.GITHUB_TOKEN }}

      - name: Run build
        run: |
          docker/build.py \
            --tag "${{ steps.tag.outputs.tag }}" \
            --arch "${{ matrix.os == 'ubuntu-24.04-arm' && 'aarch64' || 'amd64' }}" \
            --build-type "${{ matrix.build_type }}" \
            --registry ghcr \
            build ${{ steps.tag.outputs.push == 'true' && '--push --no-cache-to' || '' }} ${{ (matrix.os == 'ubuntu-24.04' && matrix.build_type == 'docker') && '--load' || '' }}

      # The amd64 "docker" image is also loaded locally (above) and handed to
      # compile-test as an artifact, so the smoke test reuses this build instead
      # of building the image a second time. Using an artifact (rather than the
      # pushed image) keeps it working for fork PRs, which never push to ghcr.io.
      - name: Export image for compile-test
        if: matrix.os == 'ubuntu-24.04' && matrix.build_type == 'docker'
        run: docker save "ghcr.io/esphome/esphome-amd64:${{ steps.tag.outputs.tag }}" | gzip > compile-test-image.tar.gz

      - name: Upload compile-test image artifact
        if: matrix.os == 'ubuntu-24.04' && matrix.build_type == 'docker'
        uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
        with:
          # The tar is already gzipped, so upload it as-is. archive: false skips
          # the redundant zip and makes the file name the artifact name (the
          # `name` input is ignored in that mode).
          path: compile-test-image.tar.gz
          retention-days: 1
          archive: false

  manifest:
    name: Push ${{ matrix.build_type }} manifest to ghcr.io
    needs: [check-docker]
    if: needs.check-docker.outputs.push == 'true'
    runs-on: ubuntu-24.04
    permissions:
      contents: read   # actions/checkout to run docker/build.py
      packages: write  # buildx imagetools writes the multi-arch tag to ghcr.io
    strategy:
      fail-fast: false
      matrix:
        build_type:
          - "ha-addon"
          - "docker"
    steps:
      - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
      - name: Set up Python
        uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0
        with:
          python-version: "3.12"
      - name: Set up Docker Buildx
        uses: docker/setup-buildx-action@bb05f3f5519dd87d3ba754cc423b652a5edd6d2c # v4.2.0

      - name: Log in to the GitHub container registry
        uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f # v4.6.0
        with:
          registry: ghcr.io
          username: ${{ github.actor }}
          password: ${{ secrets.GITHUB_TOKEN }}

      - name: Create and push manifest
        run: |
          docker/build.py \
            --tag "${{ needs.check-docker.outputs.tag }}" \
            --build-type "${{ matrix.build_type }}" \
            --registry ghcr \
            manifest

  # Smoke-test the built image by compiling one minimal config per target
  # platform / toolchain. This catches missing system dependencies in the image
  # that only surface when a given toolchain is downloaded and run. The image is
  # the amd64 "docker" build produced by check-docker (shared as an artifact).
  compile-test:
    name: Compile ${{ matrix.id }}
    needs: check-docker
    runs-on: ubuntu-24.04
    permissions:
      contents: read  # actions/checkout to load the test configs
    strategy:
      fail-fast: false
      # Modest cap so this smoke test leaves room on the shared runner pool.
      max-parallel: 8
      matrix:
        # One entry per distinct toolchain. ESP32 variants (c3/c6/s2/s3/p4)
        # share a toolchain bundle, so esp32 is exercised on the base variant
        # across the full framework x toolchain cross-product (arduino/esp-idf
        # framework, each built with the platformio and native esp-idf
        # toolchains) so both toolchains stay covered regardless of which one is
        # the default.
        id:
          - esp8266-arduino
          - esp32-arduino-platformio
          - esp32-arduino-esp-idf
          - esp32-idf-platformio
          - esp32-idf-esp-idf
          - rp2040-arduino
          - bk72xx-arduino
          - rtl87xx-arduino
          - ln882x-arduino
          - nrf52
          - host
    steps:
      - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
      - name: Download image artifact
        uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
        with:
          name: compile-test-image.tar.gz
      - name: Load image
        run: docker load --input compile-test-image.tar.gz
      - name: Compile ${{ matrix.id }}
        run: |
          docker run --rm \
            -v "${{ github.workspace }}/docker/test_configs:/config" \
            "ghcr.io/esphome/esphome-amd64:${{ needs.check-docker.outputs.tag }}" \
            compile "${{ matrix.id }}.yaml"
