From 6fdcfce3a8a4eb5f3e5db9148ea91210f150d100 Mon Sep 17 00:00:00 2001 From: Aiden Date: Sat, 30 May 2026 12:36:03 +0800 Subject: [PATCH] Update ci.yml --- .github/workflows/ci.yml | 48 ++-- .github/workflows/publish-firmware.yml | 360 ++++++++++++++++++++++--- athom-smart-plug-v5.yaml | 2 +- 3 files changed, 357 insertions(+), 53 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index e9685b5..857b1f4 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -9,34 +9,43 @@ on: - cron: '0 0 * * *' concurrency: - group: ${{ github.workflow }}-${{ github.event.pull_request.number }} + group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} cancel-in-progress: true jobs: + discover: + name: Discover ESPHome YAML files + runs-on: ubuntu-latest + outputs: + matrix: ${{ steps.files.outputs.matrix }} + steps: + - name: Checkout source code + uses: actions/checkout@v4 + + - name: Discover root-level YAML files + id: files + run: | + set -euo pipefail + + mapfile -t files < <(find . -maxdepth 1 -type f -name '*.yaml' -exec basename {} .yaml \; | sort) + if [ "${#files[@]}" -eq 0 ]; then + echo "No root-level ESPHome YAML files found." >&2 + exit 1 + fi + + printf '%s\n' "${files[@]}" + printf 'matrix=%s\n' "$(printf '%s\n' "${files[@]}" | jq -R -s -c 'split("\n")[:-1]')" >> "${GITHUB_OUTPUT}" + ci: name: Building ${{ matrix.file }} / ${{ matrix.esphome-version }} runs-on: ubuntu-latest + needs: + - discover strategy: fail-fast: false max-parallel: 3 matrix: - #### Modify below here to match your project #### - file: - - athom-2ch-relay-board - - athom-4ch-relay-board - - athom-8ch-relay-board - - athom-energy-monitor-x2 - - athom-energy-monitor-x6 - - athom-garage-door - - athom-mini-relay-v2 - - athom-smart-plug - - athom-zigbee-gateway - - athom-presence-sensor-v3 - - athom-scd40-sensor - - athom-sht40-sensor - - athom-ld2450-sensor - #### Modify above here to match your project #### - + file: ${{ fromJson(needs.discover.outputs.matrix) }} esphome-version: - stable - beta @@ -44,8 +53,9 @@ jobs: steps: - name: Checkout source code uses: actions/checkout@v4 + - name: ESPHome ${{ matrix.esphome-version }} uses: esphome/build-action@v7 with: yaml-file: ${{ matrix.file }}.yaml - version: ${{ matrix.esphome-version }} \ No newline at end of file + version: ${{ matrix.esphome-version }} diff --git a/.github/workflows/publish-firmware.yml b/.github/workflows/publish-firmware.yml index fc69677..60f93e5 100644 --- a/.github/workflows/publish-firmware.yml +++ b/.github/workflows/publish-firmware.yml @@ -1,44 +1,338 @@ name: Publish Firmware on: - release: - types: [published] + schedule: + - cron: '27 */6 * * *' + workflow_dispatch: + inputs: + esphome_version: + description: 'ESPHome version to build. Leave empty to use the latest stable release.' + required: false + type: string + force: + description: 'Recreate the GitHub release if it already exists.' + required: false + default: false + type: boolean permissions: contents: write +concurrency: + group: ${{ github.workflow }}-${{ github.event.inputs.esphome_version || 'latest-stable' }} + cancel-in-progress: false + jobs: + prepare: + name: Prepare Release + runs-on: ubuntu-latest + outputs: + esphome-version: ${{ steps.release.outputs.esphome-version }} + release-tag: ${{ steps.release.outputs.release-tag }} + release-url: ${{ steps.release.outputs.release-url }} + release-notes: ${{ steps.release.outputs.release-notes }} + should-build: ${{ steps.release.outputs.should-build }} + matrix: ${{ steps.files.outputs.matrix }} + files: ${{ steps.files.outputs.files }} + steps: + - name: Checkout source code + uses: actions/checkout@v4 + + - name: Resolve ESPHome release + id: release + env: + GH_TOKEN: ${{ github.token }} + GH_REPO: ${{ github.repository }} + INPUT_VERSION: ${{ inputs.esphome_version }} + FORCE: ${{ inputs.force }} + run: | + set -euo pipefail + + if [ -n "${INPUT_VERSION}" ]; then + esphome_version="${INPUT_VERSION#v}" + else + esphome_version="$( + gh api repos/esphome/esphome/releases/latest --jq .tag_name + )" + esphome_version="${esphome_version#v}" + fi + + if [ -z "${esphome_version}" ] || [ "${esphome_version}" = "null" ]; then + echo "Unable to resolve an ESPHome release version." >&2 + exit 1 + fi + + release_tag="esphome-${esphome_version}" + release_url="https://github.com/${GH_REPO}/releases/tag/${release_tag}" + release_notes="Firmware built automatically from ESPHome ${esphome_version} stable release." + should_build="true" + + if gh release view "${release_tag}" >/dev/null 2>&1; then + if [ "${FORCE}" = "true" ]; then + echo "Release ${release_tag} already exists and will be replaced after a successful build." + else + should_build="false" + fi + fi + + { + echo "esphome-version=${esphome_version}" + echo "release-tag=${release_tag}" + echo "release-url=${release_url}" + echo "release-notes=${release_notes}" + echo "should-build=${should_build}" + } >> "${GITHUB_OUTPUT}" + + if [ "${should_build}" = "true" ]; then + echo "Building firmware with ESPHome ${esphome_version}." + else + echo "Release ${release_tag} already exists. Nothing to build." + fi + + - name: Discover ESPHome YAML files + id: files + run: | + set -euo pipefail + + mapfile -t files < <(find . -maxdepth 1 -type f -name '*.yaml' -exec basename {} \; | sort) + if [ "${#files[@]}" -eq 0 ]; then + echo "No root-level ESPHome YAML files found." >&2 + exit 1 + fi + + json="$(printf '%s\n' "${files[@]}" | jq -R -s -c 'split("\n")[:-1]')" + names="$(printf '%s\n' "${files[@]}" | sed 's/\.yaml$//' | jq -R -s -c 'split("\n")[:-1]')" + + { + echo "matrix=${json}" + echo "files=${names}" + } >> "${GITHUB_OUTPUT}" + build-firmware: - name: Build Firmware - uses: esphome/workflows/.github/workflows/build.yml@2025.7.0 - with: - #### Modify below here to match your project #### - files: | - athom-2ch-relay-board.yaml - athom-4ch-relay-board.yaml - athom-8ch-relay-board.yaml - athom-energy-monitor-x2.yaml - athom-energy-monitor-x6.yaml - athom-garage-door.yaml - athom-mini-relay-v2.yaml - athom-smart-plug.yaml - athom-zigbee-gateway.yaml - athom-presence-sensor-v3.yaml - athom-scd40-sensor.yaml - athom-sht40-sensor.yaml - athom-ld2450-sensor.yaml - esphome-version: 2025.7.0 - combined-name: Athom-ESP32-Device - #### Modify above here to match your project #### - - release-summary: ${{ github.event.release.body }} - release-url: ${{ github.event.release.html_url }} - release-version: ${{ github.event.release.tag_name }} - - upload-to-release: - name: Upload to Release - uses: esphome/workflows/.github/workflows/upload-to-gh-release.yml@2025.7.0 + name: Build ${{ matrix.file }} + runs-on: ubuntu-latest needs: + - prepare + if: needs.prepare.outputs.should-build == 'true' + strategy: + fail-fast: false + max-parallel: 3 + matrix: + file: ${{ fromJson(needs.prepare.outputs.matrix) }} + steps: + - name: Checkout source code + uses: actions/checkout@v4 + + - name: ESPHome ${{ needs.prepare.outputs.esphome-version }} + id: build + continue-on-error: true + uses: esphome/build-action@v7 + with: + yaml-file: ${{ matrix.file }} + version: ${{ needs.prepare.outputs.esphome-version }} + complete-manifest: true + release-summary: ${{ needs.prepare.outputs.release-notes }} + release-url: ${{ needs.prepare.outputs.release-url }} + + - name: Collect firmware files + run: | + set -euo pipefail + + name="${{ matrix.file }}" + name="${name%.yaml}" + mkdir -p "firmware/${name}" + + build_name="${{ steps.build.outputs.name }}" + build_dir="" + + if [ -n "${build_name}" ] && [ -d "${build_name}" ]; then + build_dir="${build_name}" + else + mapfile -t candidates < <(find . -mindepth 2 -maxdepth 2 -name '*.factory.bin' -exec dirname {} \; | sed 's#^\./##' | sort -u) + if [ "${#candidates[@]}" -eq 1 ]; then + build_dir="${candidates[0]}" + build_name="$(basename "${build_dir}")" + fi + fi + + if [ -z "${build_dir}" ] || [ ! -d "${build_dir}" ]; then + echo "Expected ESPHome build output directory was not found." >&2 + find . -maxdepth 3 -type f \( -name '*.bin' -o -name 'manifest.json' \) -print >&2 + exit 1 + fi + + cp "${build_dir}/"*.bin "firmware/${name}/" + + factory_bin="$(find "firmware/${name}" -maxdepth 1 -name '*.factory.bin' -print -quit)" + ota_bin="$(find "firmware/${name}" -maxdepth 1 -name '*.ota.bin' -print -quit)" + + if [ -z "${factory_bin}" ] || [ -z "${ota_bin}" ]; then + echo "Expected factory and OTA binaries in '${build_dir}'." >&2 + exit 1 + fi + + if [ ! -f "${build_dir}/manifest.json" ]; then + case "${build_name}" in + *-esp32) chip_family="ESP32" ;; + *-esp32s2) chip_family="ESP32-S2" ;; + *-esp32s3) chip_family="ESP32-S3" ;; + *-esp32c3) chip_family="ESP32-C3" ;; + *-esp32c5) chip_family="ESP32-C5" ;; + *-esp32c6) chip_family="ESP32-C6" ;; + *-esp8266) chip_family="ESP8266" ;; + *) echo "Cannot infer chip family from build name '${build_name}'." >&2; exit 1 ;; + esac + + jq -n --arg chipFamily "${chip_family}" \ + --arg name "${name}" \ + --arg version "${{ needs.prepare.outputs.esphome-version }}" \ + --arg otaPath "$(basename "${ota_bin}")" \ + --arg otaMd5 "$(md5sum "${ota_bin}" | awk '{print $1}')" \ + --arg otaSha256 "$(sha256sum "${ota_bin}" | awk '{print $1}')" \ + --arg factoryPath "$(basename "${factory_bin}")" \ + --arg factoryMd5 "$(md5sum "${factory_bin}" | awk '{print $1}')" \ + --arg factorySha256 "$(sha256sum "${factory_bin}" | awk '{print $1}')" \ + '{ + name: $name, + version: $version, + home_assistant_domain: "esphome", + new_install_prompt_erase: false, + builds: [ + { + chipFamily: $chipFamily, + ota: { + path: $otaPath, + md5: $otaMd5, + sha256: $otaSha256 + }, + parts: [ + { + path: $factoryPath, + offset: 0, + md5: $factoryMd5, + sha256: $factorySha256 + } + ] + } + ] + }' > "firmware/${name}/manifest.json" + else + cp "${build_dir}/manifest.json" "firmware/${name}/manifest.json" + fi + + find "firmware/${name}" -maxdepth 1 -type f -print | sort + test -f "firmware/${name}/manifest.json" + + - name: Upload firmware artifact + uses: actions/upload-artifact@v4 + with: + name: firmware-${{ matrix.file }} + path: firmware/ + if-no-files-found: error + retention-days: 1 + + publish-release: + name: Publish Release + runs-on: ubuntu-latest + needs: + - prepare - build-firmware - with: - version: ${{ github.event.release.tag_name }} \ No newline at end of file + if: needs.prepare.outputs.should-build == 'true' + steps: + - name: Download firmware artifacts + uses: actions/download-artifact@v4 + with: + path: downloaded-artifacts + pattern: firmware-* + merge-multiple: true + + - name: Prepare release assets + env: + RELEASE_TAG: ${{ needs.prepare.outputs.release-tag }} + ESPHOME_VERSION: ${{ needs.prepare.outputs.esphome-version }} + RELEASE_NOTES: ${{ needs.prepare.outputs.release-notes }} + FILES_JSON: ${{ needs.prepare.outputs.files }} + REPOSITORY: ${{ github.repository }} + run: | + set -euo pipefail + + mkdir -p release-assets firmware + if [ -d downloaded-artifacts/firmware ]; then + cp -R downloaded-artifacts/firmware/. firmware/ + else + cp -R downloaded-artifacts/. firmware/ + fi + + jq -n --arg name "Athom ESP32 Device" \ + --arg version "${ESPHOME_VERSION}" \ + --arg homeAssistantDomain "esphome" \ + '{name: $name, version: $version, home_assistant_domain: $homeAssistantDomain, new_install_prompt_erase: false, builds: []}' \ + > release-assets/Athom-ESP32-Device.manifest.json + + for device_dir in firmware/*; do + [ -d "${device_dir}" ] || continue + device="$(basename "${device_dir}")" + + if [ -f "${device_dir}/manifest.json" ]; then + cp "${device_dir}/manifest.json" "release-assets/${device}.manifest.json" + + tmp_manifest="$(mktemp)" + jq --slurpfile manifest "${device_dir}/manifest.json" \ + '.builds += ($manifest[0].builds // [$manifest[0]])' \ + release-assets/Athom-ESP32-Device.manifest.json > "${tmp_manifest}" + mv "${tmp_manifest}" release-assets/Athom-ESP32-Device.manifest.json + fi + + cp "${device_dir}/"*.bin release-assets/ + done + + cd release-assets + shopt -s nullglob + for binary in *.bin; do + md5sum "${binary}" > "${binary}.md5" + sha256sum "${binary}" > "${binary}.sha256" + done + + expected_count="$(jq 'length' <<< "${FILES_JSON}")" + manifest_count="$(find . -maxdepth 1 -name '*.manifest.json' ! -name 'Athom-ESP32-Device.manifest.json' | wc -l)" + factory_count="$(find . -maxdepth 1 -name '*.factory.bin' | wc -l)" + ota_count="$(find . -maxdepth 1 -name '*.ota.bin' | wc -l)" + + if [ "${manifest_count}" -ne "${expected_count}" ]; then + echo "Expected ${expected_count} device manifests, found ${manifest_count}." >&2 + exit 1 + fi + + if [ "${factory_count}" -ne "${expected_count}" ]; then + echo "Expected ${expected_count} factory binaries, found ${factory_count}." >&2 + exit 1 + fi + + if [ "${ota_count}" -ne "${expected_count}" ]; then + echo "Expected ${expected_count} OTA binaries, found ${ota_count}." >&2 + exit 1 + fi + + - name: Create GitHub release + env: + GH_TOKEN: ${{ github.token }} + GH_REPO: ${{ github.repository }} + FORCE: ${{ inputs.force }} + RELEASE_TAG: ${{ needs.prepare.outputs.release-tag }} + ESPHOME_VERSION: ${{ needs.prepare.outputs.esphome-version }} + RELEASE_NOTES: ${{ needs.prepare.outputs.release-notes }} + run: | + set -euo pipefail + + if gh release view "${RELEASE_TAG}" >/dev/null 2>&1; then + if [ "${FORCE}" = "true" ]; then + gh release delete "${RELEASE_TAG}" --yes --cleanup-tag + else + echo "Release ${RELEASE_TAG} already exists." >&2 + exit 1 + fi + fi + + gh release create "${RELEASE_TAG}" release-assets/* \ + --title "ESPHome ${ESPHOME_VERSION} firmware" \ + --notes "${RELEASE_NOTES}" diff --git a/athom-smart-plug-v5.yaml b/athom-smart-plug-v5.yaml index e4fc53b..981a3bc 100644 --- a/athom-smart-plug-v5.yaml +++ b/athom-smart-plug-v5.yaml @@ -122,7 +122,7 @@ mdns: web_server: port: 80 -# version: 3 + version: 3 network: enable_ipv6: ${ipv6_enable}