name: Build images # yamllint disable-line rule:truthy on: workflow_dispatch: release: types: ["published"] schedule: - cron: "0 2 * * *" env: BUILD_TYPE: core DEFAULT_PYTHON: "3.14.2" PIP_TIMEOUT: 60 UV_HTTP_TIMEOUT: 60 UV_SYSTEM_PYTHON: "true" # Base image version from https://github.com/home-assistant/docker BASE_IMAGE_VERSION: "2026.01.0" ARCHITECTURES: '["amd64", "aarch64"]' jobs: init: name: Initialize build if: github.repository_owner == 'home-assistant' runs-on: ubuntu-latest outputs: version: ${{ steps.version.outputs.version }} channel: ${{ steps.version.outputs.channel }} publish: ${{ steps.version.outputs.publish }} architectures: ${{ env.ARCHITECTURES }} steps: - name: Checkout the repository uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - name: Set up Python ${{ env.DEFAULT_PYTHON }} uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0 with: python-version: ${{ env.DEFAULT_PYTHON }} - 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: Verify version uses: home-assistant/actions/helpers/verify-version@master with: ignore-dev: true - name: Fail if translations files are checked in run: | if [ -n "$(find homeassistant/components/*/translations -type f)" ]; then echo "Translations files are checked in, please remove the following files:" find homeassistant/components/*/translations -type f exit 1 fi - name: Download Translations run: python3 -m script.translations download env: LOKALISE_TOKEN: ${{ secrets.LOKALISE_TOKEN }} - name: Archive translations shell: bash run: find ./homeassistant/components/*/translations -name "*.json" | tar zcvf translations.tar.gz -T - - name: Upload translations uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0 with: name: translations path: translations.tar.gz if-no-files-found: error build_base: name: Build ${{ matrix.arch }} base core image if: github.repository_owner == 'home-assistant' needs: init runs-on: ${{ matrix.os }} permissions: contents: read packages: write id-token: write strategy: fail-fast: false matrix: arch: ${{ fromJson(needs.init.outputs.architectures) }} include: - arch: amd64 os: ubuntu-latest - arch: aarch64 os: ubuntu-24.04-arm steps: - name: Checkout the repository uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - name: Download nightly wheels of frontend if: needs.init.outputs.channel == 'dev' uses: dawidd6/action-download-artifact@5c98f0b039f36ef966fdb7dfa9779262785ecb05 # v14 with: github_token: ${{secrets.GITHUB_TOKEN}} repo: home-assistant/frontend branch: dev workflow: nightly.yaml workflow_conclusion: success name: wheels - name: Download nightly wheels of intents if: needs.init.outputs.channel == 'dev' uses: dawidd6/action-download-artifact@5c98f0b039f36ef966fdb7dfa9779262785ecb05 # v14 with: github_token: ${{secrets.GITHUB_TOKEN}} repo: OHF-Voice/intents-package branch: main workflow: nightly.yaml workflow_conclusion: success name: package - name: Set up Python ${{ env.DEFAULT_PYTHON }} if: needs.init.outputs.channel == 'dev' uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0 with: python-version: ${{ env.DEFAULT_PYTHON }} - name: Adjust nightly version if: needs.init.outputs.channel == 'dev' shell: bash env: UV_PRERELEASE: allow run: | python3 -m pip install "$(grep '^uv' < requirements.txt)" uv pip install packaging tomli uv pip install . python3 script/version_bump.py nightly --set-nightly-version "${{ needs.init.outputs.version }}" if [[ "$(ls home_assistant_frontend*.whl)" =~ ^home_assistant_frontend-(.*)-py3-none-any.whl$ ]]; then echo "Found frontend wheel, setting version to: ${BASH_REMATCH[1]}" frontend_version="${BASH_REMATCH[1]}" yq \ --inplace e -o json \ '.requirements = ["home-assistant-frontend=="+env(frontend_version)]' \ homeassistant/components/frontend/manifest.json sed -i "s|home-assistant-frontend==.*|home-assistant-frontend==${BASH_REMATCH[1]}|" \ homeassistant/package_constraints.txt sed -i "s|home-assistant-frontend==.*||" requirements_all.txt fi if [[ "$(ls home_assistant_intents*.whl)" =~ ^home_assistant_intents-(.*)-py3-none-any.whl$ ]]; then echo "Found intents wheel, setting version to: ${BASH_REMATCH[1]}" yq \ --inplace e -o json \ 'del(.requirements[] | select(contains("home-assistant-intents")))' \ homeassistant/components/conversation/manifest.json intents_version="${BASH_REMATCH[1]}" yq \ --inplace e -o json \ '.requirements += ["home-assistant-intents=="+env(intents_version)]' \ homeassistant/components/conversation/manifest.json sed -i "s|home-assistant-intents==.*|home-assistant-intents==${BASH_REMATCH[1]}|" \ homeassistant/package_constraints.txt sed -i "s|home-assistant-intents==.*||" requirements_all.txt fi - name: Download translations uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7.0.0 with: name: translations - name: Extract translations run: | tar xvf translations.tar.gz rm translations.tar.gz - name: Write meta info file shell: bash run: | echo "${{ github.sha }};${{ github.ref }};${{ github.event_name }};${{ github.actor }}" > rootfs/OFFICIAL_IMAGE - 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 }} - &install_cosign name: Install Cosign uses: sigstore/cosign-installer@faadad0cce49287aee09b3a48701e75088a2c6ad # v4.0.0 with: cosign-release: "v2.5.3" - name: Set up Docker Buildx uses: docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f # v3.12.0 - name: Build variables id: vars shell: bash run: | echo "base_image=ghcr.io/home-assistant/${{ matrix.arch }}-homeassistant-base:${{ env.BASE_IMAGE_VERSION }}" >> "$GITHUB_OUTPUT" echo "cache_image=ghcr.io/home-assistant/${{ matrix.arch }}-homeassistant:latest" >> "$GITHUB_OUTPUT" echo "created=$(date --rfc-3339=seconds --utc)" >> "$GITHUB_OUTPUT" - name: Verify base image signature run: | cosign verify \ --certificate-oidc-issuer https://token.actions.githubusercontent.com \ --certificate-identity-regexp "https://github.com/home-assistant/docker/.*" \ "${{ steps.vars.outputs.base_image }}" - name: Verify cache image signature id: cache continue-on-error: true run: | cosign verify \ --certificate-oidc-issuer https://token.actions.githubusercontent.com \ --certificate-identity-regexp "https://github.com/home-assistant/core/.*" \ "${{ steps.vars.outputs.cache_image }}" - name: Build base image id: build uses: docker/build-push-action@263435318d21b8e681c14492fe198d362a7d2c83 # v6.18.0 with: context: . file: ./Dockerfile platforms: ${{ steps.vars.outputs.platform }} push: true cache-from: ${{ steps.cache.outcome == 'success' && steps.vars.outputs.cache_image || '' }} build-args: | BUILD_FROM=${{ steps.vars.outputs.base_image }} tags: ghcr.io/home-assistant/${{ matrix.arch }}-homeassistant:${{ needs.init.outputs.version }} labels: | io.hass.arch=${{ matrix.arch }} io.hass.version=${{ needs.init.outputs.version }} org.opencontainers.image.created=${{ steps.vars.outputs.created }} org.opencontainers.image.version=${{ needs.init.outputs.version }} - name: Sign image run: | cosign sign --yes "ghcr.io/home-assistant/${{ matrix.arch }}-homeassistant:${{ needs.init.outputs.version }}@${{ steps.build.outputs.digest }}" build_machine: name: Build ${{ matrix.machine }} machine core image if: github.repository_owner == 'home-assistant' needs: ["init", "build_base"] runs-on: ubuntu-latest permissions: contents: read packages: write id-token: write strategy: matrix: machine: - generic-x86-64 - intel-nuc - khadas-vim3 - odroid-c2 - odroid-c4 - odroid-m1 - odroid-n2 - qemuarm-64 - qemux86-64 - raspberrypi3-64 - raspberrypi4-64 - raspberrypi5-64 - yellow - green steps: - name: Checkout the repository uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - name: Set build additional args run: | # Create general tags if [[ "${{ needs.init.outputs.version }}" =~ d ]]; then echo "BUILD_ARGS=--additional-tag dev" >> $GITHUB_ENV elif [[ "${{ needs.init.outputs.version }}" =~ b ]]; then echo "BUILD_ARGS=--additional-tag beta" >> $GITHUB_ENV else echo "BUILD_ARGS=--additional-tag stable" >> $GITHUB_ENV fi - 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 }} # home-assistant/builder doesn't support sha pinning - name: Build base image uses: home-assistant/builder@2025.11.0 with: args: | $BUILD_ARGS \ --target /data/machine \ --cosign \ --machine "${{ needs.init.outputs.version }}=${{ matrix.machine }}" publish_ha: name: Publish version files environment: ${{ needs.init.outputs.channel }} if: github.repository_owner == 'home-assistant' needs: ["init", "build_machine"] runs-on: ubuntu-latest steps: - name: Checkout the repository uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - name: Initialize git 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 uses: home-assistant/actions/helpers/version-push@master with: key: "homeassistant[]" key-description: "Home Assistant Core" version: ${{ needs.init.outputs.version }} channel: ${{ needs.init.outputs.channel }} exclude-list: '["odroid-xu","qemuarm","qemux86","raspberrypi","raspberrypi2","raspberrypi3","raspberrypi4","tinker"]' - name: Update version file (stable -> beta) if: needs.init.outputs.channel == 'stable' uses: home-assistant/actions/helpers/version-push@master with: key: "homeassistant[]" key-description: "Home Assistant Core" version: ${{ needs.init.outputs.version }} channel: beta exclude-list: '["odroid-xu","qemuarm","qemux86","raspberrypi","raspberrypi2","raspberrypi3","raspberrypi4","tinker"]' publish_container: name: Publish meta container for ${{ matrix.registry }} environment: ${{ needs.init.outputs.channel }} if: github.repository_owner == 'home-assistant' needs: ["init", "build_base"] runs-on: ubuntu-latest permissions: contents: read packages: write id-token: write strategy: fail-fast: false matrix: registry: ["ghcr.io/home-assistant", "docker.io/homeassistant"] steps: - *install_cosign - name: Login to DockerHub if: matrix.registry == 'docker.io/homeassistant' uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 # v3.7.0 with: username: ${{ secrets.DOCKERHUB_USERNAME }} password: ${{ secrets.DOCKERHUB_TOKEN }} - 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: Verify architecture image signatures shell: bash run: | ARCHS=$(echo '${{ needs.init.outputs.architectures }}' | jq -r '.[]') for arch in $ARCHS; do echo "Verifying ${arch} image signature..." cosign verify \ --certificate-oidc-issuer https://token.actions.githubusercontent.com \ --certificate-identity-regexp https://github.com/home-assistant/core/.* \ "ghcr.io/home-assistant/${arch}-homeassistant:${{ needs.init.outputs.version }}" done echo "✓ All images verified successfully" # Generate all Docker tags based on version string # Version format: YYYY.MM.PATCH, YYYY.MM.PATCHbN (beta), or YYYY.MM.PATCH.devYYYYMMDDHHMM (dev) # Examples: # 2025.12.1 (stable) -> tags: 2025.12.1, 2025.12, stable, latest, beta, rc # 2025.12.0b3 (beta) -> tags: 2025.12.0b3, beta, rc # 2025.12.0.dev202511250240 -> tags: 2025.12.0.dev202511250240, dev - name: Generate Docker metadata id: meta uses: docker/metadata-action@c299e40c65443455700f0fdfc63efafe5b349051 # v5.10.0 with: images: ${{ matrix.registry }}/home-assistant sep-tags: "," tags: | type=raw,value=${{ needs.init.outputs.version }},priority=9999 type=raw,value=dev,enable=${{ contains(needs.init.outputs.version, 'd') }} type=raw,value=beta,enable=${{ !contains(needs.init.outputs.version, 'd') }} type=raw,value=rc,enable=${{ !contains(needs.init.outputs.version, 'd') }} type=raw,value=stable,enable=${{ !contains(needs.init.outputs.version, 'd') && !contains(needs.init.outputs.version, 'b') }} type=raw,value=latest,enable=${{ !contains(needs.init.outputs.version, 'd') && !contains(needs.init.outputs.version, 'b') }} type=semver,pattern={{major}}.{{minor}},value=${{ needs.init.outputs.version }},enable=${{ !contains(needs.init.outputs.version, 'd') && !contains(needs.init.outputs.version, 'b') }} - name: Set up Docker Buildx uses: docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f # v3.7.1 - name: Copy architecture images to DockerHub if: matrix.registry == 'docker.io/homeassistant' shell: bash run: | # Use imagetools to copy image blobs directly between registries # This preserves provenance/attestations and seems to be much faster than pull/push ARCHS=$(echo '${{ needs.init.outputs.architectures }}' | jq -r '.[]') for arch in $ARCHS; do echo "Copying ${arch} image to DockerHub..." for attempt in 1 2 3; do if docker buildx imagetools create \ --tag "docker.io/homeassistant/${arch}-homeassistant:${{ needs.init.outputs.version }}" \ "ghcr.io/home-assistant/${arch}-homeassistant:${{ needs.init.outputs.version }}"; then break fi echo "Attempt ${attempt} failed, retrying in 10 seconds..." sleep 10 if [ "${attempt}" -eq 3 ]; then echo "Failed after 3 attempts" exit 1 fi done cosign sign --yes "docker.io/homeassistant/${arch}-homeassistant:${{ needs.init.outputs.version }}" done - name: Create and push multi-arch manifests shell: bash run: | # Build list of architecture images dynamically ARCHS=$(echo '${{ needs.init.outputs.architectures }}' | jq -r '.[]') ARCH_IMAGES=() for arch in $ARCHS; do ARCH_IMAGES+=("${{ matrix.registry }}/${arch}-homeassistant:${{ needs.init.outputs.version }}") done # Build list of all tags for single manifest creation # Note: Using sep-tags=',' in metadata-action for easier parsing TAG_ARGS=() IFS=',' read -ra TAGS <<< "${{ steps.meta.outputs.tags }}" for tag in "${TAGS[@]}"; do TAG_ARGS+=("--tag" "${tag}") done # Create manifest with ALL tags in a single operation (much faster!) echo "Creating multi-arch manifest with tags: ${TAGS[*]}" docker buildx imagetools create "${TAG_ARGS[@]}" "${ARCH_IMAGES[@]}" # Sign each tag separately (signing requires individual tag names) echo "Signing all tags..." for tag in "${TAGS[@]}"; do echo "Signing ${tag}" cosign sign --yes "${tag}" done echo "All manifests created and signed successfully" build_python: name: Build PyPi package environment: ${{ needs.init.outputs.channel }} needs: ["init", "build_base"] runs-on: ubuntu-latest permissions: contents: read id-token: write if: github.repository_owner == 'home-assistant' && needs.init.outputs.publish == 'true' steps: - name: Checkout the repository uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - name: Set up Python ${{ env.DEFAULT_PYTHON }} uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0 with: python-version: ${{ env.DEFAULT_PYTHON }} - name: Download translations uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7.0.0 with: name: translations - name: Extract translations run: | tar xvf translations.tar.gz rm translations.tar.gz - name: Build package shell: bash run: | # Remove dist, build, and homeassistant.egg-info # when build locally for testing! pip install build python -m build - name: Upload package to PyPI uses: pypa/gh-action-pypi-publish@ed0c53931b1dc9bd32cbe73a98c7f6766f8a527e # v1.13.0 with: skip-existing: true hassfest-image: name: Build and test hassfest image runs-on: ubuntu-latest permissions: contents: read packages: write attestations: write id-token: write needs: ["init"] if: github.repository_owner == 'home-assistant' env: HASSFEST_IMAGE_NAME: ghcr.io/home-assistant/hassfest HASSFEST_IMAGE_TAG: ghcr.io/home-assistant/hassfest:${{ needs.init.outputs.version }} steps: - name: Checkout repository uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - 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: Build Docker image uses: docker/build-push-action@263435318d21b8e681c14492fe198d362a7d2c83 # v6.18.0 with: context: . # So action will not pull the repository again file: ./script/hassfest/docker/Dockerfile load: true tags: ${{ env.HASSFEST_IMAGE_TAG }} - name: Run hassfest against core run: docker run --rm -v ${{ github.workspace }}:/github/workspace ${{ env.HASSFEST_IMAGE_TAG }} --core-path=/github/workspace - name: Push Docker image if: needs.init.outputs.channel != 'dev' && needs.init.outputs.publish == 'true' id: push uses: docker/build-push-action@263435318d21b8e681c14492fe198d362a7d2c83 # v6.18.0 with: context: . # So action will not pull the repository again file: ./script/hassfest/docker/Dockerfile push: true tags: ${{ env.HASSFEST_IMAGE_TAG }},${{ env.HASSFEST_IMAGE_NAME }}:latest - name: Generate artifact attestation if: needs.init.outputs.channel != 'dev' && needs.init.outputs.publish == 'true' uses: actions/attest-build-provenance@96278af6caaf10aea03fd8d33a09a777ca52d62f # v3.2.0 with: subject-name: ${{ env.HASSFEST_IMAGE_NAME }} subject-digest: ${{ steps.push.outputs.digest }} push-to-registry: true