name: Build supervisor

on:
  workflow_dispatch:
    inputs:
      channel:
        description: "Channel"
        required: true
        default: "dev"
      version:
        description: "Version"
        required: true
      publish:
        description: "Publish"
        required: true
        default: "false"
      stable:
        description: "Stable"
        required: true
        default: "false"
  pull_request:
    branches: ["main"]
  release:
    types: ["published"]
  push:
    branches: ["main"]
    paths:
      - "rootfs/**"
      - "supervisor/**"
      - build.yaml
      - Dockerfile
      - requirements.txt
      - setup.py

env:
  DEFAULT_PYTHON: "3.13"
  COSIGN_VERSION: "v2.5.3"
  CRANE_VERSION: "v0.20.7"
  CRANE_SHA256: "8ef3564d264e6b5ca93f7b7f5652704c4dd29d33935aff6947dd5adefd05953e"
  BUILD_NAME: supervisor
  BUILD_TYPE: supervisor

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

jobs:
  init:
    name: Initialize build
    runs-on: ubuntu-latest
    outputs:
      architectures: ${{ steps.info.outputs.architectures }}
      version: ${{ steps.version.outputs.version }}
      channel: ${{ steps.version.outputs.channel }}
      publish: ${{ steps.version.outputs.publish }}
      build_wheels: ${{ steps.requirements.outputs.build_wheels }}
    steps:
      - name: Checkout the repository
        uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
        with:
          fetch-depth: 0

      - name: Get information
        id: info
        uses: home-assistant/actions/helpers/info@master

      - name: Get version
        id: version
        uses: home-assistant/actions/helpers/version@master
        with:
          type: ${{ env.BUILD_TYPE }}

      - name: Get changed files
        id: changed_files
        if: github.event_name == 'pull_request' || github.event_name == 'push'
        uses: masesgroup/retrieve-changed-files@491e80760c0e28d36ca6240a27b1ccb8e1402c13 # v3.0.0

      - name: Check if requirements files changed
        id: requirements
        run: |
          # No wheels build necessary for releases
          if [[ "${{ github.event_name }}" == "release" ]]; then
            echo "build_wheels=false" >> "$GITHUB_OUTPUT"
          # Always build wheels for manual dispatches
          elif [[ "${{ github.event_name }}" == "workflow_dispatch" ]]; then
            echo "build_wheels=true" >> "$GITHUB_OUTPUT"
          elif [[ "${{ steps.changed_files.outputs.all }}" =~ (requirements\.txt|build\.yaml|\.github/workflows/builder\.yml) ]]; then
            echo "build_wheels=true" >> "$GITHUB_OUTPUT"
          else
            echo "build_wheels=false" >> "$GITHUB_OUTPUT"
          fi

  build:
    name: Build ${{ matrix.arch }} supervisor
    needs: init
    runs-on: ${{ matrix.runs-on }}
    permissions:
      contents: read
      id-token: write
      packages: write
    strategy:
      matrix:
        arch: ${{ fromJson(needs.init.outputs.architectures) }}
        include:
          - runs-on: ubuntu-24.04
          - runs-on: ubuntu-24.04-arm
            arch: aarch64
    env:
      WHEELS_ABI: cp313
      WHEELS_TAG: musllinux_1_2
      WHEELS_APK_DEPS: "libffi-dev;openssl-dev;yaml-dev"
      WHEELS_SKIP_BINARY: aiohttp
    steps:
      - name: Checkout the repository
        uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
        with:
          fetch-depth: 0

      - name: Write env-file for wheels build
        if: needs.init.outputs.build_wheels == 'true'
        run: |
          (
            # Fix out of memory issues with rust
            echo "CARGO_NET_GIT_FETCH_WITH_CLI=true"
          ) > .env_file

      - name: Build and publish wheels
        if: needs.init.outputs.build_wheels == 'true' && needs.init.outputs.publish == 'true'
        uses: home-assistant/wheels@e5742a69d69f0e274e2689c998900c7d19652c21 # 2025.12.0
        with:
          wheels-key: ${{ secrets.WHEELS_KEY }}
          abi: ${{ env.WHEELS_ABI }}
          tag: ${{ env.WHEELS_TAG }}
          arch: ${{ matrix.arch }}
          apk: ${{ env.WHEELS_APK_DEPS }}
          skip-binary: ${{ env.WHEELS_SKIP_BINARY }}
          env-file: true
          requirements: "requirements.txt"

      - name: Build local wheels
        if: needs.init.outputs.build_wheels == 'true' && needs.init.outputs.publish == 'false'
        uses: home-assistant/wheels@e5742a69d69f0e274e2689c998900c7d19652c21 # 2025.12.0
        with:
          wheels-host: ""
          wheels-user: ""
          wheels-key: ""
          local-wheels-repo-path: "wheels/"
          abi: ${{ env.WHEELS_ABI }}
          tag: ${{ env.WHEELS_TAG }}
          arch: ${{ matrix.arch }}
          apk: ${{ env.WHEELS_APK_DEPS }}
          skip-binary: ${{ env.WHEELS_SKIP_BINARY }}
          env-file: true
          requirements: "requirements.txt"

      - name: Upload local wheels artifact
        if: needs.init.outputs.build_wheels == 'true' && needs.init.outputs.publish == 'false'
        uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
        with:
          name: wheels-${{ matrix.arch }}
          path: wheels
          retention-days: 1

      - name: Set version
        if: needs.init.outputs.publish == 'true'
        uses: home-assistant/actions/helpers/version@master
        with:
          type: ${{ env.BUILD_TYPE }}

      - name: Set up Python ${{ env.DEFAULT_PYTHON }}
        if: needs.init.outputs.publish == 'true'
        uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
        with:
          python-version: ${{ env.DEFAULT_PYTHON }}

      - name: Install Cosign
        if: needs.init.outputs.publish == 'true'
        uses: sigstore/cosign-installer@faadad0cce49287aee09b3a48701e75088a2c6ad # v4.0.0
        with:
          cosign-release: ${{ env.COSIGN_VERSION }}

      - name: Install dirhash and calc hash
        if: needs.init.outputs.publish == 'true'
        run: |
          pip3 install setuptools dirhash
          dir_hash="$(dirhash "${{ github.workspace }}/supervisor" -a sha256 --match "*.py")"
          echo "${dir_hash}" > rootfs/supervisor.sha256

      - name: Sign supervisor SHA256
        if: needs.init.outputs.publish == 'true'
        run: |
          cosign sign-blob --yes rootfs/supervisor.sha256 --bundle rootfs/supervisor.sha256.sig

      - name: Login to GitHub Container Registry
        if: needs.init.outputs.publish == 'true'
        uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 # v3.7.0
        with:
          registry: ghcr.io
          username: ${{ github.repository_owner }}
          password: ${{ secrets.GITHUB_TOKEN }}

      - name: Set build arguments
        if: needs.init.outputs.publish == 'false'
        run: echo "BUILD_ARGS=--test" >> $GITHUB_ENV

      # home-assistant/builder doesn't support sha pinning
      - name: Build supervisor
        uses: home-assistant/builder@2026.02.1
        with:
          image: ${{ matrix.arch }}
          args: |
            $BUILD_ARGS \
            --${{ matrix.arch }} \
            --target /data \
            --cosign \
            --generic ${{ needs.init.outputs.version }}

  version:
    name: Update version
    needs: ["init", "run_supervisor", "retag_deprecated"]
    runs-on: ubuntu-latest
    steps:
      - name: Checkout the repository
        if: needs.init.outputs.publish == 'true'
        uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2

      - name: Initialize git
        if: needs.init.outputs.publish == 'true'
        uses: home-assistant/actions/helpers/git-init@master
        with:
          name: ${{ secrets.GIT_NAME }}
          email: ${{ secrets.GIT_EMAIL }}
          token: ${{ secrets.GIT_TOKEN }}

      - name: Update version file
        if: needs.init.outputs.publish == 'true'
        uses: home-assistant/actions/helpers/version-push@master
        with:
          key: ${{ env.BUILD_NAME }}
          version: ${{ needs.init.outputs.version }}
          channel: ${{ needs.init.outputs.channel }}

  run_supervisor:
    runs-on: ubuntu-latest
    name: Run the Supervisor
    needs: ["build", "init"]
    timeout-minutes: 60
    steps:
      - name: Checkout the repository
        uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2

      - name: Download local wheels artifact
        if: needs.init.outputs.build_wheels == 'true' && needs.init.outputs.publish == 'false'
        uses: actions/download-artifact@70fc10c6e5e1ce46ad2ea6f2b72d43f7d47b13c3 # v8.0.0
        with:
          name: wheels-amd64
          path: wheels

      # home-assistant/builder doesn't support sha pinning
      - name: Build the Supervisor
        if: needs.init.outputs.publish != 'true'
        uses: home-assistant/builder@2026.02.1
        with:
          args: |
            --test \
            --amd64 \
            --target /data \
            --generic runner

      - name: Pull Supervisor
        if: needs.init.outputs.publish == 'true'
        run: |
          docker pull ghcr.io/home-assistant/amd64-hassio-supervisor:${{ needs.init.outputs.version }}
          docker tag ghcr.io/home-assistant/amd64-hassio-supervisor:${{ needs.init.outputs.version }} ghcr.io/home-assistant/amd64-hassio-supervisor:runner

      - name: Create the Supervisor
        run: |
          mkdir -p /tmp/supervisor/data
          docker create --name hassio_supervisor \
            --privileged \
            --security-opt seccomp=unconfined \
            --security-opt apparmor=unconfined \
            -v /run/docker.sock:/run/docker.sock \
            -v /run/dbus:/run/dbus \
            -v /tmp/supervisor/data:/data \
            -v /etc/machine-id:/etc/machine-id:ro \
            -e SUPERVISOR_SHARE="/tmp/supervisor/data" \
            -e SUPERVISOR_NAME=hassio_supervisor \
            -e SUPERVISOR_DEV=1 \
            -e SUPERVISOR_MACHINE="qemux86-64" \
          ghcr.io/home-assistant/amd64-hassio-supervisor:runner

      - name: Start the Supervisor
        run: docker start hassio_supervisor

      - &wait_for_supervisor
        name: Wait for Supervisor to come up
        run: |
          until SUPERVISOR=$(docker inspect --format='{{.NetworkSettings.Networks.hassio.IPAddress}}' hassio_supervisor 2>/dev/null) && \
                [ -n "$SUPERVISOR" ] && [ "$SUPERVISOR" != "<no value>" ]; do
            echo "Waiting for network configuration..."
            sleep 1
          done
          echo "Waiting for Supervisor API at http://${SUPERVISOR}/supervisor/ping"
          timeout=300
          elapsed=0

          while [ $elapsed -lt $timeout ]; do
            if response=$(curl -sSf "http://${SUPERVISOR}/supervisor/ping" 2>/dev/null); then
              if echo "$response" | jq -e '.result == "ok"' >/dev/null 2>&1; then
                echo "Supervisor is up! (took ${elapsed}s)"
                exit 0
              fi
            fi

            if [ $((elapsed % 15)) -eq 0 ]; then
              echo "Still waiting... (${elapsed}s/${timeout}s)"
            fi

            sleep 5
            elapsed=$((elapsed + 5))
          done

          echo "ERROR: Supervisor failed to start within ${timeout}s"
          echo "Last response: $response"
          echo "Checking supervisor logs..."
          docker logs --tail 50 hassio_supervisor
          exit 1

      # Wait for Core to come up so subsequent steps (backup, addon install) succeed.
      # On first startup, Supervisor installs Core via the "home_assistant_core_install"
      # job (which pulls the image and then starts Core). Jobs with cleanup=True are
      # removed from the jobs list once done, so we poll until it's gone.
      - name: Wait for Core to be started
        run: |
          echo "Waiting for Home Assistant Core to be installed and started..."
          timeout=300
          elapsed=0

          while [ $elapsed -lt $timeout ]; do
            jobs=$(docker exec hassio_cli ha jobs info --no-progress --raw-json | jq -r '.data.jobs[] | select(.name == "home_assistant_core_install" and .done == false) | .name' 2>/dev/null)
            if [ -z "$jobs" ]; then
              echo "Home Assistant Core install/start complete (took ${elapsed}s)"
              exit 0
            fi

            if [ $((elapsed % 15)) -eq 0 ]; then
              echo "Core still installing... (${elapsed}s/${timeout}s)"
            fi

            sleep 5
            elapsed=$((elapsed + 5))
          done

          echo "ERROR: Home Assistant Core failed to install/start within ${timeout}s"
          docker logs --tail 50 hassio_supervisor
          exit 1

      - name: Check the Supervisor
        run: |
          echo "Checking supervisor info"
          test=$(docker exec hassio_cli ha supervisor info --no-progress --raw-json | jq -r '.result')
          if [ "$test" != "ok" ]; then
            exit 1
          fi

          echo "Checking supervisor network info"
          test=$(docker exec hassio_cli ha network info --no-progress --raw-json | jq -r '.result')
          if [ "$test" != "ok" ]; then
            exit 1
          fi

      - name: Check the Store / App
        run: |
          echo "Install Core SSH app"
          test=$(docker exec hassio_cli ha apps install core_ssh --no-progress --raw-json | jq -r '.result')
          if [ "$test" != "ok" ]; then
            exit 1
          fi

          # Make sure it actually installed
          test=$(docker exec hassio_cli ha apps info core_ssh --no-progress --raw-json | jq -r '.data.version')
          if [[ "$test" == "null" ]]; then
            exit 1
          fi

          echo "Start Core SSH app"
          test=$(docker exec hassio_cli ha apps start core_ssh --no-progress --raw-json | jq -r '.result')
          if [ "$test" != "ok" ]; then
            exit 1
          fi

          # Make sure its state is started
          test="$(docker exec hassio_cli ha apps info core_ssh --no-progress --raw-json | jq -r '.data.state')"
          if [ "$test" != "started" ]; then
            exit 1
          fi

      - name: Create full backup
        id: backup
        run: |
          test=$(docker exec hassio_cli ha backups new --no-progress --raw-json)
          if [ "$(echo $test | jq -r '.result')" != "ok" ]; then
            exit 1
          fi
          echo "slug=$(echo $test | jq -r '.data.slug')" >> "$GITHUB_OUTPUT"

      - name: Uninstall SSH app
        run: |
          test=$(docker exec hassio_cli ha apps uninstall core_ssh --no-progress --raw-json | jq -r '.result')
          if [ "$test" != "ok" ]; then
            exit 1
          fi

      - name: Restart supervisor
        run: |
          test=$(docker exec hassio_cli ha supervisor restart --no-progress --raw-json | jq -r '.result')
          if [ "$test" != "ok" ]; then
            exit 1
          fi

      - *wait_for_supervisor

      - name: Restore SSH app from backup
        run: |
          test=$(docker exec hassio_cli ha backups restore ${{ steps.backup.outputs.slug }} --app core_ssh --no-progress --raw-json | jq -r '.result')
          if [ "$test" != "ok" ]; then
            exit 1
          fi

          # Make sure it actually installed
          test=$(docker exec hassio_cli ha apps info core_ssh --no-progress --raw-json | jq -r '.data.version')
          if [[ "$test" == "null" ]]; then
            exit 1
          fi

          # Make sure its state is started
          test="$(docker exec hassio_cli ha apps info core_ssh --no-progress --raw-json | jq -r '.data.state')"
          if [ "$test" != "started" ]; then
            exit 1
          fi

      - name: Restore SSL directory from backup
        run: |
          test=$(docker exec hassio_cli ha backups restore ${{ steps.backup.outputs.slug }} --folders ssl --no-progress --raw-json | jq -r '.result')
          if [ "$test" != "ok" ]; then
            exit 1
          fi

      - name: Get supervisor logs on failiure
        if: ${{ cancelled() || failure() }}
        run: docker logs hassio_supervisor

  retag_deprecated:
    needs: ["build", "init"]
    name: Re-tag deprecated ${{ matrix.arch }} images
    if: needs.init.outputs.publish == 'true'
    runs-on: ubuntu-latest
    permissions:
      contents: read
      id-token: write
      packages: write
    strategy:
      matrix:
        arch: ["armhf", "armv7", "i386"]
    env:
      # Last available release for deprecated architectures
      FROZEN_VERSION: "2025.11.5"
    steps:
      - name: Login to GitHub Container Registry
        uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 # v3.7.0
        with:
          registry: ghcr.io
          username: ${{ github.repository_owner }}
          password: ${{ secrets.GITHUB_TOKEN }}

      - name: Install Cosign
        uses: sigstore/cosign-installer@faadad0cce49287aee09b3a48701e75088a2c6ad # v4.0.0
        with:
          cosign-release: ${{ env.COSIGN_VERSION }}

      - name: Install crane
        run: |
          curl -sLO https://github.com/google/go-containerregistry/releases/download/${{ env.CRANE_VERSION }}/go-containerregistry_Linux_x86_64.tar.gz
          echo "${{ env.CRANE_SHA256 }}  go-containerregistry_Linux_x86_64.tar.gz" | sha256sum -c -
          tar xzf go-containerregistry_Linux_x86_64.tar.gz crane
          sudo mv crane /usr/local/bin/

      - name: Re-tag deprecated image with updated version label
        run: |
          crane auth login ghcr.io -u ${{ github.repository_owner }} -p ${{ secrets.GITHUB_TOKEN }}
          crane mutate \
            --label io.hass.version=${{ needs.init.outputs.version }} \
            --tag ghcr.io/home-assistant/${{ matrix.arch }}-hassio-supervisor:${{ needs.init.outputs.version }} \
            ghcr.io/home-assistant/${{ matrix.arch }}-hassio-supervisor:${{ env.FROZEN_VERSION }}

      - name: Sign image with Cosign
        run: |
          cosign sign --yes ghcr.io/home-assistant/${{ matrix.arch }}-hassio-supervisor:${{ needs.init.outputs.version }}
