Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
78 changes: 78 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -100,3 +100,81 @@ jobs:
--arg factory_sha256 "$(sha256sum test-esp32.factory.bin | head -c 64)" \
-f tests/${{ matrix.manifest }}-manifest-template.json > /tmp/manifest.json
diff <(jq --sort-keys . /tmp/manifest.json) <(jq --sort-keys . manifest.json)

build-substitutions:
name: substitutions / esphome:${{ matrix.esphome-version }} / ${{ matrix.manifest }} / ${{ matrix.os }}
runs-on: ${{ matrix.os }}
strategy:
fail-fast: false
max-parallel: 2
matrix:
esphome-version:
- stable
- beta
- dev
manifest:
- complete
- partial
os:
- ubuntu-24.04
- ubuntu-24.04-arm
steps:
- name: Checkout
uses: actions/checkout@v4.2.2
- name: Run action
uses: ./
id: esphome-build
with:
yaml-file: tests/test-substitutions.yaml
version: ${{ matrix.esphome-version }}
release-summary: ${{ env.TEST_RELEASE_SUMMARY }}
release-url: "https://github.com/esphome/build-action"
complete-manifest: ${{ matrix.manifest == 'complete' }}
substitutions: |
device_name=test-substitutions
board=esp32dev
- name: Write version to file
run: echo ${{ steps.esphome-build.outputs.version }} > ${{ steps.esphome-build.outputs.name }}/version
- name: Upload ESPHome binary
uses: actions/upload-artifact@v4.6.2
with:
name: build-output-substitutions-${{ matrix.esphome-version }}-${{ matrix.manifest }}-${{ matrix.os }}
path: ${{ steps.esphome-build.outputs.name }}

verify-substitutions:
name: Verify substitutions output for esphome:${{ matrix.esphome-version }} with ${{ matrix.manifest }} manifest
runs-on: ${{ matrix.os }}
needs: build-substitutions
strategy:
fail-fast: false
matrix:
esphome-version:
- stable
- beta
- dev
manifest:
- complete
- partial
os:
- ubuntu-24.04
- ubuntu-24.04-arm
steps:
- name: Checkout
uses: actions/checkout@v4.2.2
- name: Download files
uses: actions/download-artifact@v4.2.1
with:
name: build-output-substitutions-${{ matrix.esphome-version }}-${{ matrix.manifest }}-${{ matrix.os }}
- name: List files
run: |-
ls -al
tree
- name: Validate json file matches ${{ matrix.manifest }} substitutions-manifest-template.json
run: |
jq -n \
--arg ota_md5 "$(md5sum test-substitutions-esp32.ota.bin | head -c 32)" \
--arg ota_sha256 "$(sha256sum test-substitutions-esp32.ota.bin | head -c 64)" \
--arg factory_md5 "$(md5sum test-substitutions-esp32.factory.bin | head -c 32)" \
--arg factory_sha256 "$(sha256sum test-substitutions-esp32.factory.bin | head -c 64)" \
-f tests/${{ matrix.manifest }}-substitutions-manifest-template.json > /tmp/manifest.json
diff <(jq --sort-keys . /tmp/manifest.json) <(jq --sort-keys . manifest.json)
12 changes: 12 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,17 @@ with:
yaml-file: my_configuration.yaml
```

To pass build-time substitutions:

```yaml
uses: esphome/build-action@v6
with:
yaml-file: my_configuration.yaml
substitutions: |
name=my-device
board_pin=GPIO4
```

This action is used by the [ESPHome publish workflow](https://github.com/esphome/workflows/blob/main/.github/workflows/publish.yml) that is used to compile firmware and publish simple GitHub pages sites for projects.

## Inputs
Expand All @@ -25,6 +36,7 @@ This action is used by the [ESPHome publish workflow](https://github.com/esphome
| `release-summary` | _None_ | A small summary of the release that will be added to the manifest file. |
| `release-url` | _None_ | A URL to the release page that will be added to the manifest file. |
| `complete-manifest` | `false` | Whether to output a complete manifest file. Defaults to output a partial manifest only. |
| `substitutions` | _None_ | Build-time substitutions passed to ESPHome via the `-s` flag. One `key=value` pair per line. |

## Outputs

Expand Down
22 changes: 22 additions & 0 deletions action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,16 @@ inputs:
description: Output complete esp-web-tools manifest.json
required: false
default: false
substitutions:
description: |
Build-time substitutions to pass to ESPHome via the `-s` flag.
Provide one `key=value` pair per line. Values may contain `=` signs.
Example:
substitutions: |
name=my-device
board_pin=GPIO4
required: false
default: ""

outputs:
name:
Expand Down Expand Up @@ -71,13 +81,25 @@ runs:
ENDOFSUMMARY
)

# Parse the multiline `substitutions` input into repeated
# `--substitution key=value` arguments for entrypoint.py.
# Blank lines (e.g. trailing newlines from a YAML block scalar) are skipped.
substitution_args=()
while IFS= read -r line; do
[[ -z "${line}" ]] && continue
substitution_args+=(--substitution "${line}")
done <<'ENDOFSUBSTITUTIONS'
${{ inputs.substitutions }}
ENDOFSUBSTITUTIONS

docker run --rm \
--workdir /github/workspace \
-v "$(pwd)":"/github/workspace" -v "$HOME:$HOME" \
--user $(id -u):$(id -g) \
-e HOME \
esphome:${{ inputs.version }} \
${{ inputs.yaml-file }} \
"${substitution_args[@]}" \
--release-summary "$summary" \
--release-url "${{ inputs.release-url }}" \
--outputs-file "$GITHUB_OUTPUT" \
Expand Down
47 changes: 38 additions & 9 deletions entrypoint.py
Original file line number Diff line number Diff line change
Expand Up @@ -46,14 +46,34 @@ def parse_args(argv):

parser.add_argument("--outputs-file", help="GitHub Outputs file", nargs="?")

parser.add_argument(
"--substitution",
metavar="KEY=VALUE",
action="append",
default=[],
dest="substitutions",
help=(
"Build-time substitution in KEY=VALUE format (repeatable). "
"Values may themselves contain '=' signs."
),
)

return parser.parse_args(argv[1:])


def compile_firmware(filename: Path) -> int:
def _substitution_args(substitutions: dict[str, str]) -> list[str]:
"""Convert a substitutions dict into ESPHome ``-s key value`` CLI args."""
args: list[str] = []
for key, value in substitutions.items():
args += ["-s", key, value]
return args


def compile_firmware(filename: Path, substitutions: dict[str, str]) -> int:
"""Compile the firmware."""
print("::group::Compile firmware")
rc = subprocess.run(
["esphome", "compile", filename],
["esphome"] + _substitution_args(substitutions) + ["compile", filename],
stdout=sys.stdout,
stderr=sys.stderr,
check=False,
Expand Down Expand Up @@ -120,12 +140,13 @@ def source_ota_bin(self, elf: Path) -> Path:
return elf.with_name("firmware.ota.bin")


def get_config(filename: Path, outputs_file: str | None) -> tuple[Config | None, int]:
def get_config(filename: Path, outputs_file: str | None, substitutions: dict[str, str]) -> tuple[Config | None, int]:
"""Get the configuration."""
print("::group::Get config")
try:
config = subprocess.check_output(
["esphome", "config", filename], stderr=sys.stderr
["esphome"] + _substitution_args(substitutions) + ["config", filename],
stderr=sys.stderr,
)
except subprocess.CalledProcessError as e:
return None, e.returncode
Expand Down Expand Up @@ -179,12 +200,13 @@ def get_config(filename: Path, outputs_file: str | None) -> tuple[Config | None,
), 0


def get_idedata(filename: Path) -> tuple[dict | None, int]:
def get_idedata(filename: Path, substitutions: dict[str, str]) -> tuple[dict | None, int]:
"""Get the IDEData."""
print("::group::Get IDEData")
try:
idedata = subprocess.check_output(
["esphome", "idedata", filename], stderr=sys.stderr
["esphome"] + _substitution_args(substitutions) + ["idedata", filename],
stderr=sys.stderr,
)
except subprocess.CalledProcessError as e:
return None, e.returncode
Expand Down Expand Up @@ -267,22 +289,29 @@ def main(argv) -> int:

filename = Path(args.configuration)

if (rc := compile_firmware(filename)) != 0:
# Parse the list of "KEY=VALUE" strings into a dict, splitting on the
# first '=' only so that values containing '=' are preserved correctly.
substitutions: dict[str, str] = {}
for item in args.substitutions:
key, _, value = item.partition("=")
substitutions[key] = value

if (rc := compile_firmware(filename, substitutions)) != 0:
return rc

esphome_version, rc = get_esphome_version(args.outputs_file)
if rc != 0:
return rc

config, rc = get_config(filename, args.outputs_file)
config, rc = get_config(filename, args.outputs_file, substitutions)
if rc != 0:
return rc

assert config is not None

file_base = Path(config.name)

idedata, rc = get_idedata(filename)
idedata, rc = get_idedata(filename, substitutions)
if rc != 0:
return rc

Expand Down
26 changes: 26 additions & 0 deletions tests/complete-substitutions-manifest-template.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
{
"name": "esphome.example-project",
"version": "3.5.0",
"home_assistant_domain": "esphome",
"new_install_prompt_erase": false,
"builds": [
{
"chipFamily": "ESP32",
"ota": {
"path": "test-substitutions-esp32.ota.bin",
"md5": "\($ota_md5)",
"sha256": "\($ota_sha256)",
"summary": "Test \"release\" summary\n* Multiple lines",
"release_url": "https://github.com/esphome/build-action"
},
"parts": [
{
"path": "test-substitutions-esp32.factory.bin",
"offset": 0,
"md5": "\($factory_md5)",
"sha256": "\($factory_sha256)"
}
]
}
]
}
18 changes: 18 additions & 0 deletions tests/partial-substitutions-manifest-template.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
{
"chipFamily": "ESP32",
"ota": {
"path": "test-substitutions-esp32.ota.bin",
"md5": "\($ota_md5)",
"sha256": "\($ota_sha256)",
"summary": "Test \"release\" summary\n* Multiple lines",
"release_url": "https://github.com/esphome/build-action"
},
"parts": [
{
"path": "test-substitutions-esp32.factory.bin",
"offset": 0,
"md5": "\($factory_md5)",
"sha256": "\($factory_sha256)"
}
]
}
8 changes: 8 additions & 0 deletions tests/test-substitutions.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
esphome:
name: "${device_name}"
project:
name: esphome.example-project
version: "3.5.0"

esp32:
board: "${board}"
Loading