From 5efa17906b482aacfe97986323a5ff2e33162800 Mon Sep 17 00:00:00 2001 From: eXPerience83 <16572400+eXPerience83@users.noreply.github.com> Date: Mon, 23 Feb 2026 19:08:51 +0100 Subject: [PATCH 1/9] Add release ZIP packaging workflows for HACS --- .github/workflows/package-test.yml | 60 ++++++++++++++++++++++++++++ .github/workflows/release.yml | 64 ++++++++++++++++++++++++++++++ hacs.json | 12 +++--- 3 files changed, 131 insertions(+), 5 deletions(-) create mode 100644 .github/workflows/package-test.yml create mode 100644 .github/workflows/release.yml diff --git a/.github/workflows/package-test.yml b/.github/workflows/package-test.yml new file mode 100644 index 00000000..93d96e25 --- /dev/null +++ b/.github/workflows/package-test.yml @@ -0,0 +1,60 @@ +name: Package Test + +on: + workflow_dispatch: + +jobs: + package: + runs-on: ubuntu-latest + + steps: + - name: Check out repository + uses: actions/checkout@v4 + + - name: Build release ZIP + run: | + set -euo pipefail + rm -f pollenlevels.zip + zip -r pollenlevels.zip custom_components/pollenlevels + + - name: Validate ZIP structure + run: | + set -euo pipefail + python - <<'PY' + import zipfile + + zip_path = "pollenlevels.zip" + required_files = { + "custom_components/pollenlevels/manifest.json", + "custom_components/pollenlevels/__init__.py", + } + + with zipfile.ZipFile(zip_path) as archive: + names = archive.namelist() + + if not names: + raise SystemExit("ZIP is empty") + + bad_paths = [ + name + for name in names + if name and not name.startswith("custom_components/") + ] + if bad_paths: + raise SystemExit( + "ZIP contains entries outside custom_components/: " + + ", ".join(sorted(bad_paths)[:10]) + ) + + missing = sorted(required_files.difference(names)) + if missing: + raise SystemExit("ZIP missing required files: " + ", ".join(missing)) + + print("ZIP structure validated.") + PY + + - name: Upload ZIP artifact + uses: actions/upload-artifact@v4 + with: + name: pollenlevels-zip + path: pollenlevels.zip diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 00000000..528de562 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,64 @@ +name: Release Asset + +on: + release: + types: [published] + +permissions: + contents: write + +jobs: + package-and-upload: + runs-on: ubuntu-latest + + steps: + - name: Check out repository + uses: actions/checkout@v4 + + - name: Build release ZIP + run: | + set -euo pipefail + rm -f pollenlevels.zip + zip -r pollenlevels.zip custom_components/pollenlevels + + - name: Validate ZIP structure + run: | + set -euo pipefail + python - <<'PY' + import zipfile + + zip_path = "pollenlevels.zip" + required_files = { + "custom_components/pollenlevels/manifest.json", + "custom_components/pollenlevels/__init__.py", + } + + with zipfile.ZipFile(zip_path) as archive: + names = archive.namelist() + + if not names: + raise SystemExit("ZIP is empty") + + bad_paths = [ + name + for name in names + if name and not name.startswith("custom_components/") + ] + if bad_paths: + raise SystemExit( + "ZIP contains entries outside custom_components/: " + + ", ".join(sorted(bad_paths)[:10]) + ) + + missing = sorted(required_files.difference(names)) + if missing: + raise SystemExit("ZIP missing required files: " + ", ".join(missing)) + + print("ZIP structure validated.") + PY + + - name: Upload ZIP to release + uses: softprops/action-gh-release@v2 + with: + files: pollenlevels.zip + overwrite_files: true diff --git a/hacs.json b/hacs.json index 99f40769..46c45e53 100644 --- a/hacs.json +++ b/hacs.json @@ -1,7 +1,9 @@ { - "name": "Pollen Levels", - "homeassistant": "2025.3.0", - "content_in_root": false, - "render_readme": true, - "hacs": "2.0.0" + "name": "Pollen Levels", + "homeassistant": "2025.3.0", + "content_in_root": false, + "render_readme": true, + "hacs": "2.0.0", + "zip_release": true, + "filename": "pollenlevels.zip", } From 0aef84ee6a7e3686964d3b9b256066d823f1b1e3 Mon Sep 17 00:00:00 2001 From: eXPerience83 <16572400+eXPerience83@users.noreply.github.com> Date: Mon, 23 Feb 2026 19:18:19 +0100 Subject: [PATCH 2/9] Fix invalid trailing comma in hacs.json --- hacs.json | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/hacs.json b/hacs.json index 46c45e53..54ed71a8 100644 --- a/hacs.json +++ b/hacs.json @@ -1,9 +1,9 @@ { - "name": "Pollen Levels", - "homeassistant": "2025.3.0", - "content_in_root": false, - "render_readme": true, - "hacs": "2.0.0", - "zip_release": true, - "filename": "pollenlevels.zip", + "name": "Pollen Levels", + "homeassistant": "2025.3.0", + "content_in_root": false, + "render_readme": true, + "hacs": "2.0.0", + "zip_release": true, + "filename": "pollenlevels.zip" } From 54126679308e44f8b9097b59ffa62cab72ddb69f Mon Sep 17 00:00:00 2001 From: eXPerience83 <16572400+eXPerience83@users.noreply.github.com> Date: Mon, 23 Feb 2026 19:38:01 +0100 Subject: [PATCH 3/9] Fix zip_release ZIP root structure in release workflows --- .github/workflows/package-test.yml | 38 +++++++++++++----------------- .github/workflows/release.yml | 38 +++++++++++++----------------- 2 files changed, 34 insertions(+), 42 deletions(-) diff --git a/.github/workflows/package-test.yml b/.github/workflows/package-test.yml index 93d96e25..5051758c 100644 --- a/.github/workflows/package-test.yml +++ b/.github/workflows/package-test.yml @@ -9,46 +9,42 @@ jobs: steps: - name: Check out repository - uses: actions/checkout@v4 + uses: actions/checkout@v6 - - name: Build release ZIP + - name: Build release ZIP (integration root at ZIP root) + shell: bash run: | set -euo pipefail - rm -f pollenlevels.zip - zip -r pollenlevels.zip custom_components/pollenlevels + rm -f "${{ github.workspace }}/pollenlevels.zip" + cd "${{ github.workspace }}/custom_components/pollenlevels" + zip "${{ github.workspace }}/pollenlevels.zip" -r ./ - - name: Validate ZIP structure + - name: Validate ZIP structure (zip_release-compatible) + shell: bash run: | set -euo pipefail python - <<'PY' import zipfile zip_path = "pollenlevels.zip" - required_files = { - "custom_components/pollenlevels/manifest.json", - "custom_components/pollenlevels/__init__.py", - } + required = {"manifest.json", "__init__.py"} - with zipfile.ZipFile(zip_path) as archive: - names = archive.namelist() + with zipfile.ZipFile(zip_path) as zf: + names = [n for n in zf.namelist() if n and not n.endswith("/")] if not names: raise SystemExit("ZIP is empty") - bad_paths = [ - name - for name in names - if name and not name.startswith("custom_components/") - ] - if bad_paths: + bad_prefix = [n for n in names if n.startswith("custom_components/")] + if bad_prefix: raise SystemExit( - "ZIP contains entries outside custom_components/: " - + ", ".join(sorted(bad_paths)[:10]) + "ZIP contains 'custom_components/' prefix; " + "zip must be integration-root only" ) - missing = sorted(required_files.difference(names)) + missing = sorted(required.difference(names)) if missing: - raise SystemExit("ZIP missing required files: " + ", ".join(missing)) + raise SystemExit("ZIP missing required root files: " + ", ".join(missing)) print("ZIP structure validated.") PY diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 528de562..36a72860 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -13,46 +13,42 @@ jobs: steps: - name: Check out repository - uses: actions/checkout@v4 + uses: actions/checkout@v6 - - name: Build release ZIP + - name: Build release ZIP (integration root at ZIP root) + shell: bash run: | set -euo pipefail - rm -f pollenlevels.zip - zip -r pollenlevels.zip custom_components/pollenlevels + rm -f "${{ github.workspace }}/pollenlevels.zip" + cd "${{ github.workspace }}/custom_components/pollenlevels" + zip "${{ github.workspace }}/pollenlevels.zip" -r ./ - - name: Validate ZIP structure + - name: Validate ZIP structure (zip_release-compatible) + shell: bash run: | set -euo pipefail python - <<'PY' import zipfile zip_path = "pollenlevels.zip" - required_files = { - "custom_components/pollenlevels/manifest.json", - "custom_components/pollenlevels/__init__.py", - } + required = {"manifest.json", "__init__.py"} - with zipfile.ZipFile(zip_path) as archive: - names = archive.namelist() + with zipfile.ZipFile(zip_path) as zf: + names = [n for n in zf.namelist() if n and not n.endswith("/")] if not names: raise SystemExit("ZIP is empty") - bad_paths = [ - name - for name in names - if name and not name.startswith("custom_components/") - ] - if bad_paths: + bad_prefix = [n for n in names if n.startswith("custom_components/")] + if bad_prefix: raise SystemExit( - "ZIP contains entries outside custom_components/: " - + ", ".join(sorted(bad_paths)[:10]) + "ZIP contains 'custom_components/' prefix; " + "zip must be integration-root only" ) - missing = sorted(required_files.difference(names)) + missing = sorted(required.difference(names)) if missing: - raise SystemExit("ZIP missing required files: " + ", ".join(missing)) + raise SystemExit("ZIP missing required root files: " + ", ".join(missing)) print("ZIP structure validated.") PY From 3d4c59418563a7e06d91980552839f125290dbe2 Mon Sep 17 00:00:00 2001 From: eXPerience83 <16572400+eXPerience83@users.noreply.github.com> Date: Mon, 23 Feb 2026 19:38:18 +0100 Subject: [PATCH 4/9] Harden HACS validate workflow permissions --- .github/workflows/validate.yml | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/.github/workflows/validate.yml b/.github/workflows/validate.yml index efa2d283..9939005f 100644 --- a/.github/workflows/validate.yml +++ b/.github/workflows/validate.yml @@ -8,14 +8,15 @@ on: - cron: "0 0 * * *" workflow_dispatch: -# No checkout needed; HACS action validates repo metadata remotely -permissions: {} +permissions: + contents: read jobs: validate-hacs: - runs-on: "ubuntu-latest" + runs-on: ubuntu-latest steps: - name: HACS validation - uses: "hacs/action@main" + uses: hacs/action@main with: - category: "integration" + category: integration + comment: false From 315f31a2ce0cba56bdd8ba9a0c82e691dbd5fc91 Mon Sep 17 00:00:00 2001 From: eXPerience83 <16572400+eXPerience83@users.noreply.github.com> Date: Mon, 23 Feb 2026 19:39:09 +0100 Subject: [PATCH 5/9] Revert HACS validate workflow permission changes --- .github/workflows/validate.yml | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/.github/workflows/validate.yml b/.github/workflows/validate.yml index 9939005f..efa2d283 100644 --- a/.github/workflows/validate.yml +++ b/.github/workflows/validate.yml @@ -8,15 +8,14 @@ on: - cron: "0 0 * * *" workflow_dispatch: -permissions: - contents: read +# No checkout needed; HACS action validates repo metadata remotely +permissions: {} jobs: validate-hacs: - runs-on: ubuntu-latest + runs-on: "ubuntu-latest" steps: - name: HACS validation - uses: hacs/action@main + uses: "hacs/action@main" with: - category: integration - comment: false + category: "integration" From 1ef0970744baac6a175a2c6869e65ae677ca1235 Mon Sep 17 00:00:00 2001 From: eXPerience83 <16572400+eXPerience83@users.noreply.github.com> Date: Mon, 23 Feb 2026 19:40:14 +0100 Subject: [PATCH 6/9] Set least-privilege permissions in package-test workflow --- .github/workflows/package-test.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/package-test.yml b/.github/workflows/package-test.yml index 5051758c..461600f5 100644 --- a/.github/workflows/package-test.yml +++ b/.github/workflows/package-test.yml @@ -6,6 +6,8 @@ on: jobs: package: runs-on: ubuntu-latest + permissions: + contents: read steps: - name: Check out repository From e2d15331cb7f4564721e3f1e4de65181744b283e Mon Sep 17 00:00:00 2001 From: eXPerience83 <16572400+eXPerience83@users.noreply.github.com> Date: Mon, 23 Feb 2026 20:21:21 +0100 Subject: [PATCH 7/9] Update package-test artifact action to v6 --- .github/workflows/package-test.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/package-test.yml b/.github/workflows/package-test.yml index 461600f5..e7e42e20 100644 --- a/.github/workflows/package-test.yml +++ b/.github/workflows/package-test.yml @@ -52,7 +52,7 @@ jobs: PY - name: Upload ZIP artifact - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v6 with: name: pollenlevels-zip path: pollenlevels.zip From dcaf6176ff19e1f26ccc534a73c13e9d9fdfc71a Mon Sep 17 00:00:00 2001 From: eXPerience83 <16572400+eXPerience83@users.noreply.github.com> Date: Mon, 23 Feb 2026 20:21:50 +0100 Subject: [PATCH 8/9] Fail release when ZIP asset file is missing --- .github/workflows/release.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 36a72860..93055996 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -58,3 +58,4 @@ jobs: with: files: pollenlevels.zip overwrite_files: true + fail_on_unmatched_files: true From 8c3f7fcabb514213a07c8005d123092d571c9444 Mon Sep 17 00:00:00 2001 From: eXPerience83 <16572400+eXPerience83@users.noreply.github.com> Date: Tue, 24 Feb 2026 15:12:53 +0100 Subject: [PATCH 9/9] Bump version to 1.9.4 and update changelog --- CHANGELOG.md | 7 +++++++ custom_components/pollenlevels/manifest.json | 2 +- pyproject.toml | 2 +- 3 files changed, 9 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8df88078..7d4bd99d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,10 @@ +## [1.9.4] - 2026-02-24 +### Changed +- Updated release packaging automation for HACS `zip_release` by validating the + integration-root ZIP layout, enabling strict release upload failure when + `pollenlevels.zip` is missing, and aligning package-test artifact upload to + `actions/upload-artifact@v6`. + ## [1.9.3] - 2026-02-14 ### Fixed - Aligned config-flow API validation with runtime parsing by requiring `dailyInfo` diff --git a/custom_components/pollenlevels/manifest.json b/custom_components/pollenlevels/manifest.json index 8a6d4eb7..42f5c1d5 100644 --- a/custom_components/pollenlevels/manifest.json +++ b/custom_components/pollenlevels/manifest.json @@ -7,5 +7,5 @@ "integration_type": "service", "iot_class": "cloud_polling", "issue_tracker": "https://github.com/eXPerience83/pollenlevels/issues", - "version": "1.9.3" + "version": "1.9.4" } diff --git a/pyproject.toml b/pyproject.toml index c29fb16d..c3d891f0 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -6,7 +6,7 @@ [project] name = "pollenlevels" -version = "1.9.3" +version = "1.9.4" # Enforce the runtime floor aligned with upcoming HA Python 3.14 images. requires-python = ">=3.14"