# Home Assistant Operating System build workflow name: OS build on: release: types: [published] workflow_dispatch: inputs: boards: description: 'List of boards to build (comma separated identifiers)' required: false type: string publish: description: 'Publish build artifacts to R2 (not applicable to forks)' required: true type: boolean default: true run_tests: description: 'Run tests after build' required: true type: boolean default: true hassio_channel: description: 'Release channel to use (default: stable for GH releases, dev otherwise)' type: choice required: true default: default options: - default - stable - beta - dev env: PYTHON_VERSION: "3.13" jobs: prepare: name: Prepare build runs-on: ubuntu-22.04 permissions: contents: read pull-requests: read packages: write outputs: version_dev: ${{ steps.version_dev.outputs.version_dev }} version_main: ${{ steps.version.outputs.version_main }} version_full: ${{ steps.version.outputs.version_full }} channel: ${{ steps.channel.outputs.channel }} hassio_channel_option: ${{ steps.channel.outputs.hassio_channel_option }} matrix: ${{ steps.generate_matrix.outputs.result }} build_container_image: ghcr.io/${{ github.repository_owner }}/haos-builder@${{ steps.build_haos_builder.outputs.digest }} publish_build: ${{ steps.check_publish.outputs.publish_build }} self_signed_cert: ${{ steps.generate_signing_key.outputs.self_signed_cert }} steps: - name: Checkout source uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: persist-credentials: false - name: Check if build should be published id: check_publish env: PUBLISH_FLAG: ${{ inputs.publish }} run: | if [ "${{ github.repository }}" == "home-assistant/operating-system" ]; then if [ "${PUBLISH_FLAG}" != "true" ] && [ "${{ github.event_name }}" != "release" ]; then echo "publish_build=false" >> "$GITHUB_OUTPUT" else echo "publish_build=true" >> "$GITHUB_OUTPUT" fi else echo "publish_build=false" >> "$GITHUB_OUTPUT" fi - name: Generate development version shell: bash id: version_dev if: ${{ github.event_name != 'release' }} env: PUBLISH_BUILD: ${{ steps.check_publish.outputs.publish_build }} run: | version_dev="dev$(date --utc +'%Y%m%d')" if [ "${{ env.PUBLISH_BUILD }}" != "true" ] || [ "${{ github.ref }}" != "refs/heads/dev" ]; then version_dev="dev$(date +%s)" fi echo "Development version \"${version_dev}\"" echo "version_dev=${version_dev}" >> $GITHUB_OUTPUT - name: Set version suffix if: ${{ github.event_name != 'release' }} env: VERSION_DEV: ${{ steps.version_dev.outputs.version_dev }} run: | sed -i -E "s/(^VERSION_SUFFIX=\").*(\"$)/\1${VERSION_DEV}\2/" buildroot-external/meta - name: Get version id: version run: | . ${GITHUB_WORKSPACE}/buildroot-external/meta echo "version_main=${VERSION_MAJOR}.${VERSION_MINOR}" >> $GITHUB_OUTPUT if [ -z "${VERSION_SUFFIX}" ]; then version_full="${VERSION_MAJOR}.${VERSION_MINOR}" else version_full="${VERSION_MAJOR}.${VERSION_MINOR}.${VERSION_SUFFIX}" fi echo "version_full=${version_full}" >> $GITHUB_OUTPUT echo "Full version number of this release is \"${version_full}\"." - name: Validate version id: version_check if: ${{ github.event_name == 'release' }} run: | if [ "${{ steps.version.outputs.version_full }}" != "${{ github.event.release.tag_name }}" ]; then echo "Version number in Buildroot metadata does not match tag (${{ steps.version.outputs.version_full }} vs ${{ github.event.release.tag_name }})." exit 1 fi - name: Get channel id: channel run: | if [[ "${{ github.event_name }}" == "release" ]]; then if [[ "${{ github.event.release.prerelease }}" == "true" ]]; then echo "channel=beta" >> "$GITHUB_OUTPUT" else echo "channel=stable" >> "$GITHUB_OUTPUT" fi else echo "channel=dev" >> "$GITHUB_OUTPUT" fi if [[ "${{ inputs.hassio_channel }}" == "default" ]]; then if [[ "${{ github.event_name }}" == "release" ]]; then echo "hassio_channel_option=BR2_PACKAGE_HASSIO_CHANNEL_STABLE" >> "$GITHUB_OUTPUT" else echo "hassio_channel_option=BR2_PACKAGE_HASSIO_CHANNEL_DEV" >> "$GITHUB_OUTPUT" fi else if [[ "${{ inputs.hassio_channel }}" == "stable" ]]; then echo "hassio_channel_option=BR2_PACKAGE_HASSIO_CHANNEL_STABLE" >> "$GITHUB_OUTPUT" elif [[ "${{ inputs.hassio_channel }}" == "beta" ]]; then echo "hassio_channel_option=BR2_PACKAGE_HASSIO_CHANNEL_BETA" >> "$GITHUB_OUTPUT" elif [[ "${{ inputs.hassio_channel }}" == "dev" ]]; then echo "hassio_channel_option=BR2_PACKAGE_HASSIO_CHANNEL_DEV" >> "$GITHUB_OUTPUT" fi fi - name: Create build matrix uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 id: generate_matrix with: script: | const boards = require('./.github/workflows/matrix.json') if ("${{ github.event_name }}" == "release") { return { "board": boards } } const boardFilter = "${{ github.event.inputs.boards }}" const runTests = "${{ github.event.inputs.run_tests }}" === "true" if (boardFilter == "") { console.log("Run full build for all boards") return { "board": boards } } else { console.log("Run partial build") const boardSet = new Set(boardFilter.split(",")) // if tests are enabled, we need to ensure the OVA board is included if (runTests && !boardSet.has("ova")) { console.log("Adding OVA board for integration tests") boardSet.add("ova") } const buildBoards = boards.filter(b => boardSet.has(b.id)) return { "board": buildBoards } } - name: Set up Docker Buildx uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd # v4.0.0 - name: Log in to the GitHub container registry uses: docker/login-action@b45d80f862d83dbcd57f89517bcf500b2ab88fb2 # v4.0.0 with: registry: ghcr.io username: ${{ github.repository_owner }} password: ${{ secrets.GITHUB_TOKEN }} - name: Build and Push uses: docker/build-push-action@d08e5c354a6adb9ed34480a06d141179aa583294 # v7.0.0 id: build_haos_builder with: context: . file: Dockerfile tags: ghcr.io/${{ github.repository_owner }}/haos-builder cache-from: ghcr.io/${{ github.repository_owner }}/haos-builder:cache-${{ steps.version.outputs.version_main }} cache-to: ghcr.io/${{ github.repository_owner }}/haos-builder:cache-${{ steps.version.outputs.version_main }} push: true - name: Generate self-signed certificate id: generate_signing_key env: RAUC_CERTIFICATE: ${{ secrets.RAUC_CERTIFICATE }} RAUC_PRIVATE_KEY: ${{ secrets.RAUC_PRIVATE_KEY }} if: env.RAUC_CERTIFICATE == '' || env.RAUC_PRIVATE_KEY == '' run: | echo "::warning:: RAUC certificate or key is missing in the repository secrets. Building with a public self-signed certificate!" buildroot-external/scripts/generate-signing-key.sh cert.pem key.pem echo "self_signed_cert=true" >> $GITHUB_OUTPUT - name: Create signing key uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0 if: steps.generate_signing_key.outcome == 'success' with: name: signing-key path: | cert.pem key.pem build: name: Build for ${{ matrix.board.id }} permissions: contents: write # for actions/upload-release-asset to upload release asset needs: prepare strategy: fail-fast: ${{ github.event_name == 'release' }} matrix: ${{ fromJson(needs.prepare.outputs.matrix) }} runs-on: ubuntu-22.04 steps: - name: Checkout source uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: submodules: true persist-credentials: false - name: Setup Python version ${{ env.PYTHON_VERSION }} if: ${{ github.event_name != 'release' }} uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0 with: python-version: ${{ env.PYTHON_VERSION }} - name: Install AWS CLI if: ${{ github.event_name != 'release' && needs.prepare.outputs.publish_build == 'true' }} run: pip install 'awscli<1.37.0' - name: Set version suffix if: ${{ github.event_name != 'release' }} env: VERSION_DEV: ${{ needs.prepare.outputs.version_dev }} run: | sed -i -E "s/(^VERSION_SUFFIX=\").*(\"$)/\1${VERSION_DEV}\2/" buildroot-external/meta - name: 'Add release PKI certs' if: ${{ needs.prepare.outputs.self_signed_cert != 'true' }} env: RAUC_CERTIFICATE: ${{ secrets.RAUC_CERTIFICATE }} RAUC_PRIVATE_KEY: ${{ secrets.RAUC_PRIVATE_KEY }} run: | echo -e "-----BEGIN CERTIFICATE-----\n${RAUC_CERTIFICATE}\n-----END CERTIFICATE-----" > cert.pem echo -e "-----BEGIN PRIVATE KEY-----\n${RAUC_PRIVATE_KEY}\n-----END PRIVATE KEY-----" > key.pem - name: Get self-signed certificate from the prepare job if: ${{ needs.prepare.outputs.self_signed_cert == 'true' }} uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 with: name: signing-key - name: Free space on build drive run: | # Inspired by https://github.com/easimon/maximize-build-space/blob/v7/action.yml df -h sudo rm -rf /usr/local/lib/android/sdk/ndk sudo rm -rf /opt/hostedtoolcache/CodeQL sudo mkdir /mnt/cache sudo mkdir /mnt/output WORKSPACE_OWNER="$(stat -c '%U:%G' "${GITHUB_WORKSPACE}")" # output directory is symlinked for easier access from workspace # but for build container it must be mounted as a volume sudo ln -sf /mnt/output "${GITHUB_WORKSPACE}/output" sudo chown -R "${WORKSPACE_OWNER}" /mnt/cache sudo chown -R "${WORKSPACE_OWNER}" /mnt/output df -h - name: "Restore cache: object files" uses: actions/cache/restore@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4 with: path: /mnt/cache/cc key: haos-cc-${{ matrix.board.id }} - name: Generate build config uses: "./.github/actions/haos-builder-command" with: image: ${{ needs.prepare.outputs.build_container_image }} command: make ${{ matrix.board.defconfig }}_defconfig - name: Override release channel if: ${{ needs.prepare.outputs.hassio_channel_option != 'BR2_PACKAGE_HASSIO_CHANNEL_STABLE' }} uses: "./.github/actions/haos-builder-command" with: image: ${{ needs.prepare.outputs.build_container_image }} command: | bash -c 'echo "${{ needs.prepare.outputs.hassio_channel_option }}=y" >> /build/output/.config && make olddefconfig' - name: Build uses: "./.github/actions/haos-builder-command" with: image: ${{ needs.prepare.outputs.build_container_image }} command: make - name: Check Linux config uses: "./.github/actions/haos-builder-command" with: image: ${{ needs.prepare.outputs.build_container_image }} command: | make BR2_CHECK_DOTCONFIG_OPTS="--github-format --strip-path-prefix=/build/" linux-check-dotconfig - name: Upload artifacts if: ${{ github.event_name != 'release' && needs.prepare.outputs.publish_build == 'true' }} working-directory: output/images/ env: AWS_ACCESS_KEY_ID: ${{ secrets.R2_OS_ARTIFACTS_ID }} AWS_SECRET_ACCESS_KEY: ${{ secrets.R2_OS_ARTIFACTS_KEY }} run: | aws s3 sync \ ./ \ s3://${{ secrets.R2_OS_ARTIFACTS_BUCKET }}/${{ needs.prepare.outputs.version_full }}/ \ --exclude "*" \ --include "haos_*" \ --endpoint-url ${{ secrets.R2_OS_ARTIFACTS_ENDPOINT }} - name: Upload release assets if: ${{ github.event_name == 'release' }} uses: shogo82148/actions-upload-release-asset@96bc1f0cb850b65efd58a6b5eaa0a69f88d38077 # v1.10.0 with: upload_url: ${{ github.event.release.upload_url }} asset_path: output/images/haos_* - name: Print cache stats run: | echo "Cache size: $(du -sh /mnt/cache/cc)" echo "Files total: $(find /mnt/cache/cc -mindepth 1 -type f | wc -l)" echo "Old files to remove: $(find /mnt/cache/cc -mindepth 1 -type f -not -anewer output/Makefile | wc -l)" find /mnt/cache/cc -mindepth 1 -type f -not -anewer output/Makefile -delete echo "Cache size after pruning: $(du -sh /mnt/cache/cc)" - name: "Save cache: object files" if: github.ref == 'refs/heads/dev' uses: actions/cache/save@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4 with: path: /mnt/cache/cc key: haos-cc-${{ matrix.board.id }}-${{ github.run_id }} - name: Generate build summary run: | echo "# ${{ matrix.board.id }} build summary" >> $GITHUB_STEP_SUMMARY echo "## Built-in OS components" >> $GITHUB_STEP_SUMMARY echo "Release channel: ${{ inputs.hassio_channel }} (${{ needs.prepare.outputs.hassio_channel_option }})" >> $GITHUB_STEP_SUMMARY echo "| Container | Version |" >> $GITHUB_STEP_SUMMARY echo "|:-|:-|" >> $GITHUB_STEP_SUMMARY supervisor_version=$(jq -r ".supervisor" output/build/hassio-*/version.json) landingpage_version=$(curl -fsSL https://api.github.com/repos/home-assistant/landingpage/releases/latest | jq -r '.tag_name') echo "| supervisor | [${supervisor_version}](https://github.com/home-assistant/supervisor/releases/tag/${supervisor_version}) |" >> $GITHUB_STEP_SUMMARY echo "| landingpage | [${landingpage_version}](https://github.com/home-assistant/landingpage/releases/tag/${landingpage_version}) |" >> $GITHUB_STEP_SUMMARY for plugin in dns audio cli multicast observer; do version=$(jq -r ".${plugin}" output/build/hassio-*/version.json) echo "| plugin-${plugin} | [${version}](https://github.com/home-assistant/plugin-${plugin}/releases/tag/${version}) |" >> $GITHUB_STEP_SUMMARY done echo "## Artifacts" >> $GITHUB_STEP_SUMMARY echo "| File | Size (bytes) | Size (formatted) |" >> $GITHUB_STEP_SUMMARY echo "|:-|:-|:-|" >> $GITHUB_STEP_SUMMARY for f in output/images/haos_*; do echo "| $(basename $f) | $(du -b $f | cut -f1) | $(du -bh $f | cut -f1) |" >> $GITHUB_STEP_SUMMARY done echo "## Partitions" >> $GITHUB_STEP_SUMMARY echo "| File | Size (bytes) | Size (formatted) |" >> $GITHUB_STEP_SUMMARY echo "|:-|:-|:-|" >> $GITHUB_STEP_SUMMARY for f in boot.vfat kernel.img rootfs.erofs overlay.ext4 data.ext4; do echo "| ${f} | $(du -b output/images/$f | cut -f1) | $(du -bh output/images/$f | cut -f1) |" >> $GITHUB_STEP_SUMMARY done - name: Upload OS image artifact uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0 if: ${{ github.event_name != 'release' && needs.prepare.outputs.publish_build != 'true' && matrix.board.id != 'ova' }} with: archive: false name: haos_${{ matrix.board.id }}-${{ needs.prepare.outputs.version_full }}.img.xz path: | output/images/haos_${{ matrix.board.id }}-${{ needs.prepare.outputs.version_full }}.img.xz - name: Upload RAUC bundle artifact uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0 if: ${{ github.event_name != 'release' && needs.prepare.outputs.publish_build != 'true' }} with: archive: false name: haos_${{ matrix.board.id }}-${{ needs.prepare.outputs.version_full }}.raucb path: | output/images/haos_${{ matrix.board.id }}-${{ needs.prepare.outputs.version_full }}.raucb - name: Upload Open Virtualization Format (OVA) artifact uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0 if: ${{ github.event_name != 'release' && needs.prepare.outputs.publish_build != 'true' && matrix.board.id == 'ova' }} with: archive: false name: haos_${{ matrix.board.id }}-${{ needs.prepare.outputs.version_full }}.ova path: | output/images/haos_${{ matrix.board.id }}-${{ needs.prepare.outputs.version_full }}.ova - name: Upload QEMU disk image artifact uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0 # Create artifact for ova every time - it's used by the called tests workflow if: ${{ matrix.board.id == 'ova' || (github.event_name != 'release' && needs.prepare.outputs.publish_build != 'true' && matrix.board.id == 'generic-aarch64') }} with: archive: false name: haos_${{ matrix.board.id }}-${{ needs.prepare.outputs.version_full }}.qcow2.xz path: | output/images/haos_${{ matrix.board.id }}-${{ needs.prepare.outputs.version_full }}.qcow2.xz - name: Upload VMware Virtual Machine Disk (VMDK) artifact uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0 if: ${{ github.event_name != 'release' && needs.prepare.outputs.publish_build != 'true' && (matrix.board.id == 'generic-aarch64' || matrix.board.id == 'ova') }} with: archive: false name: haos_${{ matrix.board.id }}-${{ needs.prepare.outputs.version_full }}.vmdk.zip path: | output/images/haos_${{ matrix.board.id }}-${{ needs.prepare.outputs.version_full }}.vmdk.zip - name: Upload VirtualBox Virtual Disk Image (VDI) artifact uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0 if: ${{ github.event_name != 'release' && needs.prepare.outputs.publish_build != 'true' && matrix.board.id == 'ova' }} with: archive: false name: haos_${{ matrix.board.id }}-${{ needs.prepare.outputs.version_full }}.vdi.zip path: | output/images/haos_${{ matrix.board.id }}-${{ needs.prepare.outputs.version_full }}.vdi.zip - name: Upload Virtual Hard Disk v2 (VHDX) artifact uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0 if: ${{ github.event_name != 'release' && needs.prepare.outputs.publish_build != 'true' && matrix.board.id == 'ova' }} with: archive: false name: haos_${{ matrix.board.id }}-${{ needs.prepare.outputs.version_full }}.vhdx.zip path: | output/images/haos_${{ matrix.board.id }}-${{ needs.prepare.outputs.version_full }}.vhdx.zip test: name: Test OS image if: ${{ github.event_name == 'release' || inputs.run_tests == true }} needs: [ build, prepare ] uses: ./.github/workflows/test.yaml with: version: ${{ needs.prepare.outputs.version_full }} update_index: name: Update artifacts index if: ${{ github.event_name != 'release' && needs.prepare.outputs.publish_build == 'true' }} needs: [ build, prepare ] uses: home-assistant/operating-system/.github/workflows/artifacts-index.yaml@dev with: version: ${{ needs.prepare.outputs.version_full }} secrets: R2_OS_ARTIFACTS_ID: ${{ secrets.R2_OS_ARTIFACTS_ID }} R2_OS_ARTIFACTS_KEY: ${{ secrets.R2_OS_ARTIFACTS_KEY }} R2_OS_ARTIFACTS_BUCKET: ${{ secrets.R2_OS_ARTIFACTS_BUCKET }} R2_OS_ARTIFACTS_ENDPOINT: ${{ secrets.R2_OS_ARTIFACTS_ENDPOINT }} CF_ZONE: ${{ secrets.CF_ZONE }} CF_PURGE_TOKEN: ${{ secrets.CF_PURGE_TOKEN }} bump_version: name: Bump ${{ needs.prepare.outputs.channel }} channel version if: ${{ github.repository == 'home-assistant/operating-system' && needs.prepare.outputs.publish_build == 'true' && (needs.prepare.outputs.channel != 'dev' || github.ref == 'refs/heads/dev') }} environment: ${{ needs.prepare.outputs.channel }} needs: [ build, prepare ] runs-on: ubuntu-22.04 steps: - name: Checkout source uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: persist-credentials: false - 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: Bump Home Assistant OS ${{ needs.prepare.outputs.channel }} channel version uses: home-assistant/actions/helpers/version-push@master with: key: "hassos[]" key-description: "Home Assistant OS" version: ${{ needs.prepare.outputs.version_full }} channel: ${{ needs.prepare.outputs.channel }} - name: Bump Home Assistant OS beta channel version on stable release if: ${{ needs.prepare.outputs.channel == 'stable' }} uses: home-assistant/actions/helpers/version-push@master with: key: "hassos[]" key-description: "Home Assistant OS" version: ${{ needs.prepare.outputs.version_full }} channel: beta - name: Bump stable Home Assistant version for RPi Imager if: ${{ github.event_name == 'release' && needs.prepare.outputs.channel == 'stable' }} uses: "./.github/actions/bump-rpi-imager-version" with: version: ${{ needs.prepare.outputs.version_full }} release-date: ${{ github.event.release.published_at }}