name: Build ESPHome Firmware on: push: tags: - "v*" workflow_dispatch: inputs: version: description: "Version tag (e.g., 2026.1.5)" required: true default: "2026.1.5" jobs: build: runs-on: ubuntu-latest strategy: fail-fast: false matrix: include: # Dimmer LED v0.3 - yaml_file: boneio-dimmer_8ch-v0_3.yaml board_name: "Dimmer LED" board_value: "dimmer" version: "v0.3" version_key: "v0.3" output_option: "8 Channels" chip_family: "ESP32" - yaml_file: boneio-dimmer_2rgbw-v0_3.yaml board_name: "Dimmer LED" board_value: "dimmer" version: "v0.3" version_key: "v0.3" output_option: "2xRGBW" chip_family: "ESP32" - yaml_file: boneio-dimmer_4cct-v0_3.yaml board_name: "Dimmer LED" board_value: "dimmer" version: "v0.3" version_key: "v0.3" output_option: "4 CCT" chip_family: "ESP32" # Dimmer LED gen2 v0.1 - yaml_file: boneio-dimmer_gen2_8ch-v0_1.yaml board_name: "Dimmer LED gen2" board_value: "dimmer_g2" version: "v0.1" version_key: "v0.1" output_option: "8 Channels" chip_family: "ESP32-S3" - yaml_file: boneio-dimmer_gen2_2rgbw-v0_1.yaml board_name: "Dimmer LED gen2" board_value: "dimmer_g2" version: "v0.1" version_key: "v0.1" output_option: "2xRGBW" chip_family: "ESP32-S3" - yaml_file: boneio-dimmer_gen2_4cct-v0_1.yaml board_name: "Dimmer LED gen2" board_value: "dimmer_g2" version: "v0.1" version_key: "v0.1" output_option: "4 CCT" chip_family: "ESP32-S3" # Input24 gen2 - yaml_file: boneio-input24_gen2-v0_1.yaml board_name: "boneIO ESP Input24 gen2" board_value: "input24" version: "v0.1" version_key: "v0.1" output_option: "None" chip_family: "ESP32-S3" # 8x10A gen2 - yaml_file: boneio-8x10A_gen2_lights-v0_1.yaml board_name: "boneIO ESP 8x10A gen2" board_value: "8x10A" version: "v0.1" version_key: "v0.1" output_option: "Lights" chip_family: "ESP32-S3" # 32x10 v0.7 - yaml_file: boneio-32x10_lights_v0_7.yaml board_name: "boneIO ESP 32x10" board_value: "32x10" version: "v0.7 PP" version_key: "v0.7" output_option: "Lights" chip_family: "ESP32" - yaml_file: boneio-32x10_switches_v0_7.yaml board_name: "boneIO ESP 32x10" board_value: "32x10" version: "v0.7 PP" version_key: "v0.7" output_option: "Switches" chip_family: "ESP32" # 32x10 v0.6 - yaml_file: boneio-32x10_lights_v0_5-v0_6.yaml board_name: "boneIO ESP 32x10" board_value: "32x10" version: "v0.5/v0.6 PP" version_key: "v0.6" output_option: "Lights" chip_family: "ESP32" - yaml_file: boneio-32x10_switches_v0_5-v0_6.yaml board_name: "boneIO ESP 32x10" board_value: "32x10" version: "v0.5/v0.6 PP" version_key: "v0.6" output_option: "Switches" chip_family: "ESP32" # 24x16 - yaml_file: boneio-24x16_switches_v0_7.yaml board_name: "boneIO ESP 24x16" board_value: "24x16" version: "v0.7 MP" version_key: "v0.7" output_option: "Switches" chip_family: "ESP32" - yaml_file: boneio-24x16_switches_v0_5-v0_6.yaml board_name: "boneIO ESP 24x16" board_value: "24x16" version: "v0.5/v0.6 MP" version_key: "v0.6" output_option: "Switches" chip_family: "ESP32" # Cover - yaml_file: boneio-cover_v0_7.yaml board_name: "boneIO ESP Cover" board_value: "cover" version: "v0.7 PP" version_key: "v0.7" output_option: "" chip_family: "ESP32" - yaml_file: boneio-cover_v0_5-v0_6.yaml board_name: "boneIO ESP Cover" board_value: "cover" version: "v0.5/v0.6 PP" version_key: "v0.6" output_option: "" chip_family: "ESP32" # Cover Mix - yaml_file: boneio-cover_mix_lights_v0_7.yaml board_name: "boneIO ESP Cover Mix" board_value: "mix" version: "v0.7 PP" version_key: "v0.7" output_option: "Lights" chip_family: "ESP32" - yaml_file: boneio-cover_mix_switches_v0_7.yaml board_name: "boneIO ESP Cover Mix" board_value: "mix" version: "v0.7 PP" version_key: "v0.7" output_option: "Switches" chip_family: "ESP32" - yaml_file: boneio-cover_mix_lights_v0_5-v0_6.yaml board_name: "boneIO ESP Cover Mix" board_value: "mix" version: "v0.5/v0.6 PP" version_key: "v0.6" output_option: "Lights" chip_family: "ESP32" - yaml_file: boneio-cover_mix_switches_v0_5-v0_6.yaml board_name: "boneIO ESP Cover Mix" board_value: "mix" version: "v0.5/v0.6 PP" version_key: "v0.6" output_option: "Switches" chip_family: "ESP32" steps: - name: Checkout repository uses: actions/checkout@v4 - name: Build firmware uses: esphome/build-action@v7 id: esphome-build with: yaml-file: ${{ matrix.yaml_file }} version: latest complete-manifest: true - name: Save build metadata run: | mkdir -p metadata cat > metadata/${{ steps.esphome-build.outputs.original-name }}.json << 'METADATA_EOF' { "firmware_name": "${{ steps.esphome-build.outputs.original-name }}", "yaml_file": "${{ matrix.yaml_file }}", "board_name": "${{ matrix.board_name }}", "board_value": "${{ matrix.board_value }}", "version": "${{ matrix.version }}", "version_key": "${{ matrix.version_key }}", "output_option": "${{ matrix.output_option }}", "chip_family": "${{ matrix.chip_family }}" } METADATA_EOF - name: Upload firmware artifact uses: actions/upload-artifact@v4 with: name: firmware-${{ steps.esphome-build.outputs.original-name }} path: ${{ steps.esphome-build.outputs.name }} - name: Upload metadata artifact uses: actions/upload-artifact@v4 with: name: metadata-${{ steps.esphome-build.outputs.original-name }} path: metadata/ deploy: needs: build runs-on: ubuntu-latest permissions: contents: write pages: write id-token: write steps: - name: Download firmware artifacts uses: actions/download-artifact@v4 with: path: artifacts pattern: firmware-* merge-multiple: true - name: Download metadata artifacts uses: actions/download-artifact@v4 with: path: metadata pattern: metadata-* merge-multiple: true - name: Get version id: version run: | if [ "${{ github.event_name }}" == "workflow_dispatch" ]; then echo "version=${{ github.event.inputs.version }}" >> $GITHUB_OUTPUT else echo "version=${GITHUB_REF#refs/tags/v}" >> $GITHUB_OUTPUT fi - name: Prepare GitHub Pages content run: | mkdir -p gh-pages/firmware gh-pages/manifests release export VERSION="${{ steps.version.outputs.version }}" export BUILD_DATE=$(date -u +"%Y-%m-%dT%H:%M:%SZ") export GITHUB_PAGES_URL="https://boneio-eu.github.io/esphome" # Debug: show artifact structure echo "=== Artifacts structure ===" find artifacts/ -type f | head -80 echo "=== End artifacts ===" # Copy and rename firmware binaries # esphome/build-action outputs: name-chip/name-chip.factory.bin and name-chip.ota.bin for factory_bin in $(find artifacts/ -name '*.factory.bin'); do BASENAME=$(basename "$factory_bin") # Remove .factory from name: boneio-dr-8ch-03-esp32.factory.bin -> boneio-dr-8ch-03-esp32.bin SIMPLE_NAME="${BASENAME/.factory.bin/.bin}" cp "$factory_bin" "gh-pages/firmware/${SIMPLE_NAME}" echo " Copied to gh-pages/firmware/${SIMPLE_NAME}" done # Copy all binaries (factory + ota) to release folder for GitHub Release find artifacts/ -name '*.factory.bin' -exec cp {} release/ \; find artifacts/ -name '*.ota.bin' -exec cp {} release/ \; echo "=== Release files ===" ls -la release/ echo "=== End release ===" # Generate manifest JSONs + firmware-catalog.json from metadata python3 << 'PYTHON_EOF' import json import glob import os version = os.environ.get("VERSION", "unknown") build_date = os.environ.get("BUILD_DATE", "unknown") github_pages_url = os.environ.get("GITHUB_PAGES_URL", "") # Read all metadata files builds = [] for meta_file in glob.glob("metadata/*.json"): with open(meta_file) as f: builds.append(json.load(f)) # Discover actual firmware files on gh-pages to map metadata -> binary fw_files = {} for f in glob.glob("gh-pages/firmware/*.bin"): fw_files[os.path.basename(f)] = f print(f"Found {len(fw_files)} firmware files: {list(fw_files.keys())}") # Generate individual ESP Web Tools manifests for build in builds: fw_name = build["firmware_name"] chip = build["chip_family"] chip_suffix = chip.lower().replace("-", "") # ESP32-S3 -> esp32s3 # Find matching binary file bin_filename = f"{fw_name}-{chip_suffix}.bin" if bin_filename not in fw_files: # Fallback: try without chip suffix bin_filename = f"{fw_name}.bin" if bin_filename not in fw_files: print(f" WARNING: No binary found for {fw_name} (tried {fw_name}-{chip_suffix}.bin)") # List candidates candidates = [k for k in fw_files if fw_name in k] if candidates: bin_filename = candidates[0] print(f" Using candidate: {bin_filename}") else: continue manifest = { "name": f'{build["board_name"]} {build["output_option"]} {build["version"]}'.strip(), "version": version, "builds": [ { "chipFamily": chip, "parts": [ { "path": f'{github_pages_url}/firmware/{bin_filename}', "offset": 0 } ] } ] } manifest_path = f'gh-pages/manifests/{fw_name}.json' with open(manifest_path, 'w') as f: json.dump(manifest, f, indent=2) print(f' Generated manifest: {fw_name}.json -> {bin_filename}') # Group by board for catalog boards_map = {} for build in builds: board_value = build["board_value"] if board_value not in boards_map: boards_map[board_value] = { "name": build["board_name"], "value": board_value, "versions_map": {} } version_key = build["version_key"] if version_key not in boards_map[board_value]["versions_map"]: boards_map[board_value]["versions_map"][version_key] = { "version": build["version"], "versionKey": version_key, "options": [] } manifest_url = f'{github_pages_url}/manifests/{build["firmware_name"]}.json' yaml_url = f'https://github.com/boneIO-eu/esphome/blob/main/{build["yaml_file"]}' option = { "name": build["output_option"] if build["output_option"] else "Standard", "manifest_url": manifest_url, "yaml_url": yaml_url } boards_map[board_value]["versions_map"][version_key]["options"].append(option) # Convert to final structure boards = [] for board_value, board_data in boards_map.items(): versions = list(board_data["versions_map"].values()) boards.append({ "name": board_data["name"], "value": board_data["value"], "versions": versions }) catalog = { "version": version, "build_date": build_date, "boards": boards } with open("gh-pages/firmware-catalog.json", "w") as f: json.dump(catalog, f, indent=2) print(f"Generated firmware-catalog.json with {len(boards)} boards") print(f"Generated {len(builds)} manifest files") PYTHON_EOF # Create version file echo "$VERSION" > gh-pages/version.txt # Create index page cat > gh-pages/index.html << 'EOF' boneIO Firmware

boneIO ESPHome Firmware

Firmware Catalog (JSON API)

Manifests

EOF - name: Deploy to GitHub Pages uses: peaceiris/actions-gh-pages@v4 with: github_token: ${{ secrets.GITHUB_TOKEN }} publish_dir: ./gh-pages force_orphan: true - name: Create GitHub Release uses: softprops/action-gh-release@v2 with: tag_name: v${{ steps.version.outputs.version }} name: Firmware v${{ steps.version.outputs.version }} files: release/* generate_release_notes: true