Files
hassio-addons/.github/workflows/onpush_builder.yaml
Alexandre 39af3e9453 Refactor GitHub Actions workflow for clarity and updates
Refactor GitHub Actions workflow for improved readability and functionality. Update paths, permissions, and steps for detecting changes and sanitizing files.
2025-11-26 06:54:36 +01:00

345 lines
12 KiB
YAML

# yamllint disable rule:line-length
---
name: Builder
on:
workflow_call:
push:
branches: [master]
paths:
- "**/config.*"
permissions:
contents: write
packages: write
concurrency:
group: builder-${{ github.ref }}
cancel-in-progress: true
env:
BUILD_ARGS: ""
jobs:
detect-changed-addons:
if: ${{ github.repository_owner == 'alexbelgium' && !contains(github.event.head_commit.message, 'nobuild') }}
runs-on: ubuntu-latest
outputs:
changedAddons: ${{ steps.find.outputs.changed_addons }}
hasChanges: ${{ steps.find.outputs.has_changes }}
steps:
- name: Checkout repo
uses: actions/checkout@v6 [oai_citation:0‡GitHub](https://github.com/actions/checkout?utm_source=chatgpt.com)
with:
fetch-depth: 0
- name: Find changed add-on directories (config.json|yaml|yml)
id: find
shell: bash
run: |
set -euo pipefail
BASE_SHA="${{ github.event.before }}"
if [[ -z "${BASE_SHA}" || "${BASE_SHA}" == "0000000000000000000000000000000000000000" ]]; then
# workflow_call or unusual event: fall back to previous commit
BASE_SHA="$(git rev-parse HEAD~1)"
fi
mapfile -t files < <(git diff --name-only "${BASE_SHA}" "${{ github.sha }}" \
| grep -E '^[^/]+/config\.(json|ya?ml)$' || true)
echo "Changed config files:"
printf '%s\n' "${files[@]:-}"
if (( ${#files[@]} == 0 )); then
echo 'changed_addons=[]' >> "$GITHUB_OUTPUT"
echo 'has_changes=false' >> "$GITHUB_OUTPUT"
exit 0
fi
changed_addons="$(printf '%s\n' "${files[@]}" | awk -F/ '{print $1}' | sort -u | jq -R -s -c 'split("\n")[:-1]')"
echo "Changed addons: ${changed_addons}"
echo "changed_addons=${changed_addons}" >> "$GITHUB_OUTPUT"
echo "has_changes=true" >> "$GITHUB_OUTPUT"
prebuild-sanitize:
if: ${{ needs.detect-changed-addons.outputs.hasChanges == 'true' }}
needs: detect-changed-addons
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6 [oai_citation:1‡GitHub](https://github.com/actions/checkout?utm_source=chatgpt.com)
with:
fetch-depth: 0
- name: Sanitize tracked text files (Unicode spaces + CRLF→LF) without changing modes
shell: bash
run: |
set -euo pipefail
python3 - <<'PY'
import json, os, pathlib, re, stat, tempfile
changed_addons = json.loads(os.environ["CHANGED_ADDONS"])
repo = pathlib.Path(".").resolve()
# Unicode "space-like" chars you were targeting
space_re = re.compile(r"[\u00A0\u2002\u2003\u2007\u2008\u2009\u202F\u205F\u3000\u200B]")
def is_probably_text(data: bytes) -> bool:
# cheap binary check: NUL byte
return b"\0" not in data
def rewrite_preserving_metadata(path: pathlib.Path, new_bytes: bytes) -> None:
st = path.stat()
tmp = path.with_name(path.name + ".tmp_sanitize")
tmp.write_bytes(new_bytes)
os.chmod(tmp, stat.S_IMODE(st.st_mode))
os.utime(tmp, (st.st_atime, st.st_mtime))
os.replace(tmp, path)
for addon in changed_addons:
addon_path = repo / addon
if not addon_path.exists():
continue
# Only tracked files inside the addon
import subprocess
res = subprocess.run(
["git", "ls-files", "-z", "--", str(addon_path)],
check=True,
stdout=subprocess.PIPE,
)
files = [pathlib.Path(p.decode("utf-8")) for p in res.stdout.split(b"\0") if p]
for f in files:
try:
data = f.read_bytes()
except OSError:
continue
if not is_probably_text(data):
continue
# Decode "best effort" but keep bytes stable via surrogateescape
try:
text = data.decode("utf-8", errors="surrogateescape")
except Exception:
continue
new_text = space_re.sub(" ", text)
# Normalize CRLF and stray CR to LF
new_text = new_text.replace("\r\n", "\n").replace("\r", "\n")
if new_text != text:
rewrite_preserving_metadata(f, new_text.encode("utf-8", errors="surrogateescape"))
PY
env:
CHANGED_ADDONS: ${{ needs.detect-changed-addons.outputs.changedAddons }}
- name: Add exec bit only where appropriate (never remove exec)
shell: bash
run: |
set -euo pipefail
# Add +x only when:
# - file already executable (leave it alone), OR
# - it has a shebang, OR
# - it's a known s6/openrc-style run script location
python3 - <<'PY'
import json, os, pathlib, stat
changed_addons = json.loads(os.environ["CHANGED_ADDONS"])
repo = pathlib.Path(".").resolve()
def should_be_executable(p: pathlib.Path) -> bool:
rel = str(p.as_posix())
if rel.endswith(".sh"):
# .sh often sourced, so require shebang to enforce executability
pass
# common executable script locations in HA add-ons
if "/etc/cont-init.d/" in rel or "/etc/cont-finish.d/" in rel or "/etc/services.d/" in rel:
return True
if "/etc/s6-overlay/s6-rc.d/" in rel and rel.endswith("/run"):
return True
return False
def has_shebang(p: pathlib.Path) -> bool:
try:
with p.open("rb") as f:
return f.read(2) == b"#!"
except OSError:
return False
for addon in changed_addons:
addon_path = repo / addon
if not addon_path.exists():
continue
for p in addon_path.rglob("*"):
if not p.is_file():
continue
st = p.stat()
mode = stat.S_IMODE(st.st_mode)
is_exec = bool(mode & stat.S_IXUSR)
if is_exec:
continue # do not touch existing executable files
if has_shebang(p) or should_be_executable(p):
os.chmod(p, mode | stat.S_IXUSR | stat.S_IXGRP | stat.S_IXOTH)
PY
env:
CHANGED_ADDONS: ${{ needs.detect-changed-addons.outputs.changedAddons }}
- name: Commit sanitize if needed
shell: bash
run: |
set -euo pipefail
if git diff --quiet; then
echo "No sanitize changes."
exit 0
fi
git config user.name "github-actions[bot]"
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
git add -A
git commit -m "GitHub bot: sanitize (spaces/LF/exec) [nobuild]"
git pull --rebase --autostash
git push
lint_config:
if: ${{ needs.detect-changed-addons.outputs.hasChanges == 'true' }}
needs: [detect-changed-addons, prebuild-sanitize]
runs-on: ubuntu-latest
continue-on-error: true
strategy:
fail-fast: false
matrix:
addon: ${{ fromJSON(needs.detect-changed-addons.outputs.changedAddons) }}
steps:
- uses: actions/checkout@v6 [oai_citation:2‡GitHub](https://github.com/actions/checkout?utm_source=chatgpt.com)
- name: Run Home Assistant Add-on Lint
uses: frenck/action-addon-linter@v2 [oai_citation:3‡GitHub](https://github.com/frenck/action-addon-linter)
with:
path: "./${{ matrix.addon }}"
build:
if: ${{ needs.detect-changed-addons.outputs.hasChanges == 'true' }}
needs: [detect-changed-addons, lint_config]
runs-on: ubuntu-latest
name: Build ${{ matrix.arch }} ${{ matrix.addon }} add-on
strategy:
fail-fast: false
matrix:
addon: ${{ fromJSON(needs.detect-changed-addons.outputs.changedAddons) }}
arch: ["aarch64", "amd64", "armv7"]
steps:
- uses: actions/checkout@v6 [oai_citation:4‡GitHub](https://github.com/actions/checkout?utm_source=chatgpt.com)
- name: Get information
id: info
uses: home-assistant/actions/helpers/info@master
with:
path: "./${{ matrix.addon }}"
- name: Skip if no Dockerfile
id: dockerfile
shell: bash
run: |
if [[ -f "./${{ matrix.addon }}/Dockerfile" ]]; then
echo "has=true" >> "$GITHUB_OUTPUT"
else
echo "has=false" >> "$GITHUB_OUTPUT"
fi
- name: Check if add-on supports arch
id: check
shell: bash
run: |
if [[ "${{ steps.info.outputs.architectures }}" =~ ${{ matrix.arch }} ]]; then
echo "build_arch=true" >> "$GITHUB_OUTPUT"
echo "image=$(echo "${{ steps.info.outputs.image }}" | cut -d'/' -f3)" >> "$GITHUB_OUTPUT"
else
echo "build_arch=false" >> "$GITHUB_OUTPUT"
fi
- name: Login to GitHub Container Registry
if: ${{ env.BUILD_ARGS != '--test' }}
uses: docker/login-action@v3.6.0
with:
registry: ghcr.io
username: ${{ github.repository_owner }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Build ${{ matrix.addon }} add-on
if: ${{ steps.check.outputs.build_arch == 'true' && steps.dockerfile.outputs.has == 'true' }}
uses: home-assistant/builder@2025.09.0 [oai_citation:5‡GitHub](https://github.com/home-assistant/builder/releases?utm_source=chatgpt.com)
env:
CAS_API_KEY: ${{ secrets.CAS_API_KEY }}
with:
args: |
${{ env.BUILD_ARGS }} \
--${{ matrix.arch }} \
--target "/data/${{ matrix.addon }}" \
--image "${{ steps.check.outputs.image }}" \
--docker-hub "ghcr.io/${{ github.repository_owner }}" \
--addon
make-changelog:
if: ${{ needs.detect-changed-addons.outputs.hasChanges == 'true' }}
needs: [detect-changed-addons, build]
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
addon: ${{ fromJSON(needs.detect-changed-addons.outputs.changedAddons) }}
steps:
- uses: actions/checkout@v6 [oai_citation:6‡GitHub](https://github.com/actions/checkout?utm_source=chatgpt.com)
with:
fetch-depth: 0
- name: Update changelog for minor versions
shell: bash
run: |
set -euo pipefail
cd "${{ matrix.addon }}"
if [[ -f config.yaml ]]; then
version="$(yq -r '.version // ""' config.yaml 2>/dev/null || true)"
elif [[ -f config.json ]]; then
version="$(jq -r '.version // ""' config.json 2>/dev/null || true)"
else
exit 0
fi
version="$(echo "$version" | xargs)"
[[ -z "$version" ]] && exit 0
[[ "$version" == *"test"* ]] && exit 0
touch CHANGELOG.md
if ! grep -qF "## $version " CHANGELOG.md; then
first_line="$(sed -n '/./p' CHANGELOG.md | head -n 1 || true)"
if [[ "$first_line" != "-"* ]]; then
sed -i "1i\\- Minor bugs fixed" CHANGELOG.md
fi
sed -i "1i\\## $version ($(date '+%d-%m-%Y'))" CHANGELOG.md
fi
- name: Commit changelog if needed
shell: bash
run: |
set -euo pipefail
if git diff --quiet; then
echo "No changelog changes."
exit 0
fi
git config user.name "github-actions[bot]"
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
git add -A
git commit -m "GitHub bot: changelog [nobuild]"
git pull --rebase --autostash
git push