mirror of
https://github.com/alexbelgium/hassio-addons.git
synced 2026-08-20 03:47:20 +02:00
* refactor(webtop,webtop_kde,claude_desktop): share the Selkies startup scripts All three add-ons are built on the LinuxServer Selkies base image and had independently drifted copies of the same startup scripts. claude_desktop's copies carry a set of fixes the two webtops never received, so make claude_desktop the single source and symlink the shared scripts from webtop_kde/rootfs (which webtop/rootfs already symlinks in full). Shared by symlink: 20-folders.sh, 21-gpu_permissions.sh, 80-configuration.sh, 90-ingress.sh and the six etc/nginx/includes files. Kept add-on specific: everything Claude-only stays in claude_desktop (81/82/83/84 tool installs, 85-openbox_autostart.sh, defaults/, usr/local/bin, svc-headroom), and everything webtop-only stays in webtop_kde (90-ssl.sh, helpers/microsoft-edge-stable, and the new 81-microsoft_edge.sh). To make the shared scripts add-on agnostic: - 20-folders.sh derives its default data location from the home directory the Dockerfile baked into the abc user instead of hardcoding /data/data. That yields /data/data on claude_desktop and /config/data_kde on both webtops, matching each add-on's previous behaviour exactly. - The permission_mode: bypass root guard is skipped on add-ons that do not declare that option. - 80-configuration.sh falls back to pip when the image does not ship uv. - The Microsoft Edge install moves out of 80-configuration.sh into a webtop-only 81-microsoft_edge.sh, which also absorbs the ownership fixup that used to run in 20-folders.sh before Edge was installed and so never matched anything. CI: the builder's symlink-resolution step made a single pass over a pre-computed file list, so resolving webtop/rootfs (a directory symlink) could copy the symlinks inside it verbatim, leaving links that escape the webtop build context. Verified on this tree: the old loop leaves 10 dangling symlinks under webtop/. Extract it to .github/scripts/resolve_symlinks.sh, repeat until a pass finds nothing, and run it in the PR check too, which had no resolution step at all. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix: guard two startup aborts found in review Both are crash paths in code added by this PR, not hardening: - 20-folders.sh: getent exits 2 when the user does not exist, and under bashio's `set -o pipefail` plus the script's `set -e` that aborts at the assignment, so the "could not read abc's home" fallback below it was unreachable. Same trap already documented in 21-gpu_permissions.sh. Verified: without the guard the shell exits 2; with it the fallback runs. - 81-microsoft_edge.sh: the ownership fixup lost the `-f` guard the original had in 20-folders.sh. Without nullglob an unmatched /usr/bin/microsoft-edge* reaches chown as a literal and `set -e` kills container startup. Now a nullglob array with a warning when empty. Also make resolve_symlinks.sh fail on a broken symlink instead of deleting it. Dropping it silently yields an image that builds clean and misbehaves at runtime; a red build is easier to diagnose. Verified both paths: the repo as-is resolves to 0 symlinks and exit 0, and an injected broken link exits 1. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix: address review comments on data-location default and Edge downloads Two findings from CodeRabbit, both correctness rather than hardening: - 20-folders.sh rewrites abc's home in /etc/passwd further down, so re-reading it on the next boot returned the *previously selected* location as the image default. On a restart that reuses the container's writable layer, clearing data_location would strand the user on their old custom path instead of restoring the built-in one. Cache the value in /etc/.addon_image_home, which shares the writable layer's lifetime with the edit it compensates for: a rebuilt or recreated container starts from a pristine /etc/passwd and regenerates it. Simulated all three cases (first boot, reused container with a rewritten passwd, recreated container) under `set -e` + `set -o pipefail`. - 81-microsoft_edge.sh: both curl calls were unbounded, so a stalled packages.microsoft.com would hang cont-init.d and with it the whole add-on. Add --fail/--connect-timeout/--max-time and skip the install with a logged error when version discovery or the download fails. The desktop is useful without Edge; an add-on wedged before Selkies starts is not. Not addressed, deliberately: escaping $LOCATION/$DEFAULT_LOCATION for sed, and validating symlink targets in resolve_symlinks.sh. Both are hardening against inputs that are not reachable in normal use, both predate this PR, and the maintainer has asked to prioritise usability over that class of change. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
287 lines
12 KiB
YAML
287 lines
12 KiB
YAML
# yamllint disable rule:line-length
|
||
# shellcheck disable=SC2043
|
||
---
|
||
name: PR Check Build
|
||
|
||
on:
|
||
pull_request:
|
||
branches:
|
||
- master
|
||
|
||
jobs:
|
||
# 1. Detect which add-on folders changed (by config.json|yaml|yml modification)
|
||
check-addon-changes:
|
||
if: ${{ github.repository_owner == 'alexbelgium' && !contains(github.event.head_commit.message, 'nobuild') }}
|
||
runs-on: ubuntu-latest
|
||
outputs:
|
||
changedAddons: ${{ steps.find_addons.outputs.changed_addons }}
|
||
changedChangelogFiles: ${{ steps.changed-files.outputs.changelogs_files }}
|
||
steps:
|
||
- name: Checkout repo
|
||
uses: actions/checkout@v7.0.1
|
||
with:
|
||
# Need the merge commit's parents resolvable (HEAD^1 below): a depth-1 shallow
|
||
# checkout truncates parent refs entirely at the boundary commit.
|
||
fetch-depth: 2
|
||
- name: Find changed addon directories
|
||
id: find_addons
|
||
run: |
|
||
# github.event.pull_request.base.sha is a snapshot from event-trigger time and can be
|
||
# stale if master advances before checkout; HEAD^1 is the actual base this merge
|
||
# commit was built against, so it's always correct.
|
||
base_sha=$(git rev-parse HEAD^1)
|
||
git fetch origin "$base_sha"
|
||
diff_files=$(git diff --name-only "$base_sha" "${{ github.sha }}")
|
||
changed_config_files=$(printf '%s\n' "$diff_files" | grep -E '^[^/]+/config\.(json|ya?ml)$' || true)
|
||
echo "Changed config files:"
|
||
echo "$changed_config_files"
|
||
changed_addons=$(printf '%s' "$changed_config_files" | awk -F/ '{print $1}' | sort -u | jq -R -s -c 'split("\n") | map(select(length > 0))')
|
||
echo "Changed addons: $changed_addons"
|
||
echo "changed_addons=$changed_addons" >> "$GITHUB_OUTPUT"
|
||
- name: Find changelog
|
||
id: changed-files
|
||
run: |
|
||
base_sha=$(git rev-parse HEAD^1)
|
||
git fetch origin "$base_sha"
|
||
diff_files=$(git diff --name-only "$base_sha" "${{ github.sha }}")
|
||
changed_changelog_files=$(printf '%s\n' "$diff_files" | grep -iE '^([^/]+/)?changelog\.(md|txt|ya?ml|json)$' || true)
|
||
echo "$changed_changelog_files"
|
||
{
|
||
echo "changelogs_files<<EOF_CHANGELOG_FILES"
|
||
echo "$changed_changelog_files"
|
||
echo "EOF_CHANGELOG_FILES"
|
||
} >> "$GITHUB_OUTPUT"
|
||
changed_config_files=$(printf '%s\n' "$diff_files" | grep -E '^[^/]+/config\.(json|ya?ml)$' || true)
|
||
echo "$changed_config_files"
|
||
all_changed_files=$(echo -e "$changed_config_files\n$changed_changelog_files" | sort -u)
|
||
changed_addons=$(printf '%s' "$all_changed_files" | awk -F/ '{print $1}' | sort -u | jq -R -s -c 'split("\n") | map(select(length > 0))')
|
||
echo "Changed addons: $changed_addons"
|
||
echo "changed_addons=$changed_addons" >> "$GITHUB_OUTPUT"
|
||
|
||
check-changed-changelog:
|
||
name: Check if CHANGELOG.md changed
|
||
needs: check-addon-changes
|
||
if: ${{ needs.check-addon-changes.outputs.changedAddons != '[]' }}
|
||
runs-on: ubuntu-latest
|
||
strategy:
|
||
fail-fast: false
|
||
matrix:
|
||
addon: ${{ fromJSON(needs.check-addon-changes.outputs.changedAddons) }}
|
||
steps:
|
||
- name: 🔎 Check for updated CHANGELOG.md
|
||
shell: bash
|
||
run: |
|
||
# shellcheck disable=SC2076,SC2059
|
||
if [[ ! "${{ needs.check-addon-changes.outputs.changedChangelogFiles }}" =~ "${{ matrix.addon }}/CHANGELOG.md" ]]; then
|
||
echo "::error::No new entries in ${{ matrix.addon }} CHANGELOG.md file!"
|
||
exit 1
|
||
fi
|
||
|
||
addon-linter:
|
||
name: Addon linting
|
||
needs: check-addon-changes
|
||
if: ${{ needs.check-addon-changes.outputs.changedAddons != '[]' }}
|
||
runs-on: ubuntu-latest
|
||
strategy:
|
||
fail-fast: false
|
||
matrix:
|
||
addon: ${{ fromJSON(needs.check-addon-changes.outputs.changedAddons) }}
|
||
steps:
|
||
- name: ↩️ Checkout
|
||
uses: actions/checkout@v7.0.1
|
||
|
||
- name: 🔎 Run Home Assistant Add-on Lint
|
||
uses: frenck/action-addon-linter@v2
|
||
with:
|
||
path: "./${{ matrix.addon }}"
|
||
|
||
check-build:
|
||
name: Test addon build
|
||
needs: check-addon-changes
|
||
if: ${{ needs.check-addon-changes.outputs.changedAddons != '[]' }}
|
||
runs-on: ubuntu-latest
|
||
strategy:
|
||
fail-fast: false
|
||
matrix:
|
||
addon: ${{ fromJSON(needs.check-addon-changes.outputs.changedAddons) }}
|
||
steps:
|
||
- name: ↩️ Checkout
|
||
uses: actions/checkout@v7.0.1
|
||
|
||
- name: Resolve symlinks in repository copy
|
||
run: bash .github/scripts/resolve_symlinks.sh
|
||
|
||
- name: Copy templates into addon build context
|
||
env:
|
||
ADDON: ${{ matrix.addon }}
|
||
run: |
|
||
set -euo pipefail
|
||
TEMPLATES_DIR=".templates"
|
||
ADDON_DIR="./$ADDON"
|
||
|
||
# Keep PR builds aligned with the production builder.
|
||
for script in ha_automodules.sh ha_autoapps.sh ha_entrypoint.sh bashio-standalone.sh ha_lsio.sh; do
|
||
if [ -f "$TEMPLATES_DIR/$script" ]; then
|
||
cp "$TEMPLATES_DIR/$script" "$ADDON_DIR/$script"
|
||
fi
|
||
done
|
||
|
||
- name: ℹ️ Gather addon info
|
||
id: information
|
||
uses: frenck/action-addon-information@v1.4
|
||
with:
|
||
path: "./${{ matrix.addon }}/"
|
||
|
||
- name: 🗄️ Cache docker layers
|
||
uses: actions/cache@v6
|
||
with:
|
||
path: /tmp/buildx-cache
|
||
key: ${{ runner.os }}-buildx-${{ matrix.addon }}-${{ hashFiles('**/Dockerfile') }}
|
||
restore-keys: |
|
||
${{ runner.os }}-buildx-${{ matrix.addon }}-
|
||
|
||
- name: 🔖 Create addon image tags
|
||
id: tags
|
||
shell: bash
|
||
run: |
|
||
imagetemplate="${{ steps.information.outputs.image }}"
|
||
version="${{ steps.information.outputs.version }}"
|
||
echo "Using imagetemplate '${imagetemplate}'"
|
||
{
|
||
echo "armhf=${imagetemplate/\{arch\}/armhf}:${version}"
|
||
echo "armv7=${imagetemplate/\{arch\}/armv7}:${version}"
|
||
echo "aarch64=${imagetemplate/\{arch\}/aarch64}:${version}"
|
||
echo "amd64=${imagetemplate/\{arch\}/amd64}:${version}"
|
||
echo "i386=${imagetemplate/\{arch\}/i386}:${version}"
|
||
} >> "$GITHUB_OUTPUT"
|
||
|
||
- name: 🏷️ Create addon labels
|
||
id: labels
|
||
shell: bash
|
||
run: |
|
||
labels="io.hass.version=${{ steps.information.outputs.version }}"
|
||
labels=$(printf '%s' "$labels\nio.hass.name=${{ steps.information.outputs.name }}")
|
||
labels=$(printf '%s' "$labels\nio.hass.description=${{ steps.information.outputs.description }}")
|
||
labels=$(printf '%s' "$labels\nio.hass.type=addon")
|
||
labels=$(printf '%s' "$labels\nio.hass.url=${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}/tree/master/${{ matrix.addon }}")
|
||
labels=$(printf '%s' "$labels\norg.opencontainers.image.title=${{ steps.information.outputs.name }}")
|
||
labels=$(printf '%s' "$labels\norg.opencontainers.image.description=${{ steps.information.outputs.description }}")
|
||
labels=$(printf '%s' "$labels\norg.opencontainers.image.version=${{ steps.information.outputs.version }}")
|
||
labels=$(printf '%s' "$labels\norg.opencontainers.image.authors=Poeschl <Poeschl@users.noreply.github.com>")
|
||
labels=$(printf '%s' "$labels\norg.opencontainers.image.url=${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}")
|
||
labels=$(printf '%s' "$labels\norg.opencontainers.image.source=${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}/tree/master/${{ matrix.addon }}")
|
||
labels=$(printf '%s' "$labels\norg.opencontainers.image.created=$(date -Is)")
|
||
labels=$(printf '%s' "$labels\norg.opencontainers.image.revision=${GITHUB_SHA}")
|
||
echo "Generic labels: $labels"
|
||
armhf_labels=$(printf '%s' "$labels\nio.hass.arch=armhf")
|
||
armv7_labels=$(printf '%s' "$labels\nio.hass.arch=armv7")
|
||
aarch64_labels=$(printf '%s' "$labels\nio.hass.arch=aarch64")
|
||
amd64_labels=$(printf '%s' "$labels\nio.hass.arch=amd64")
|
||
i386_labels=$(printf '%s' "$labels\nio.hass.arch=i386")
|
||
armhf_labels="${armhf_labels//$'\n'/'%0A'}"
|
||
armv7_labels="${armv7_labels//$'\n'/'%0A'}"
|
||
aarch64_labels="${aarch64_labels//$'\n'/'%0A'}"
|
||
amd64_labels="${amd64_labels//$'\n'/'%0A'}"
|
||
i386_labels="${i386_labels//$'\n'/'%0A'}"
|
||
{
|
||
echo "armhf=$armhf_labels"
|
||
echo "armv7=$armv7_labels"
|
||
echo "aarch64=$aarch64_labels"
|
||
echo "amd64=$amd64_labels"
|
||
echo "i386=$i386_labels"
|
||
} >> "$GITHUB_OUTPUT"
|
||
|
||
- name: 💽 Create addon build-args
|
||
id: build_args
|
||
shell: bash
|
||
run: |
|
||
{
|
||
echo "armhf=BUILD_FROM=$(jq -r .build_from.armhf // empty ${{ steps.information.outputs.build }})"
|
||
echo "armv7=BUILD_FROM=$(jq -r .build_from.armv7 // empty ${{ steps.information.outputs.build }})"
|
||
echo "aarch64=BUILD_FROM=$(jq -r .build_from.aarch64 // empty ${{ steps.information.outputs.build }})"
|
||
echo "amd64=BUILD_FROM=$(jq -r .build_from.amd64 // empty ${{ steps.information.outputs.build }})"
|
||
echo "i386=BUILD_FROM=$(jq -r .build_from.i386 // empty ${{ steps.information.outputs.build }})"
|
||
} >> "$GITHUB_OUTPUT"
|
||
|
||
- name: 🏗️ Set up QEMU
|
||
uses: docker/setup-qemu-action@v4
|
||
|
||
- name: 🏗️ Set up Docker Buildx
|
||
uses: docker/setup-buildx-action@v4
|
||
|
||
- name: 💿 Build Addon - armhf
|
||
if: ${{ steps.information.outputs.armhf == 'true' }}
|
||
uses: docker/build-push-action@v7
|
||
with:
|
||
context: ${{ matrix.addon }}
|
||
push: false
|
||
load: true
|
||
file: ${{ matrix.addon }}/Dockerfile
|
||
tags: ${{ steps.tags.outputs.armhf }}
|
||
labels: ${{ steps.labels.outputs.armhf }}
|
||
build-args: ${{ steps.build_args.outputs.armhf }}
|
||
cache-from: type=local,src=/tmp/buildx-cache/armhf
|
||
cache-to: type=local,dest=/tmp/buildx-cache-new/armhf
|
||
|
||
- name: 💿 Build Addon - armv7
|
||
if: ${{ steps.information.outputs.armv7 == 'true' }}
|
||
uses: docker/build-push-action@v7
|
||
with:
|
||
context: ${{ matrix.addon }}
|
||
push: false
|
||
load: true
|
||
file: ${{ matrix.addon }}/Dockerfile
|
||
tags: ${{ steps.tags.outputs.armv7 }}
|
||
labels: ${{ steps.labels.outputs.armv7 }}
|
||
build-args: ${{ steps.build_args.outputs.armv7 }}
|
||
cache-from: type=local,src=/tmp/buildx-cache/armv7
|
||
cache-to: type=local,dest=/tmp/buildx-cache-new/armv7
|
||
|
||
- name: 💿 Build Addon - aarch64
|
||
if: ${{ steps.information.outputs.aarch64 == 'true' }}
|
||
uses: docker/build-push-action@v7
|
||
with:
|
||
context: ${{ matrix.addon }}
|
||
platforms: linux/arm64
|
||
push: false
|
||
load: true
|
||
file: ${{ matrix.addon }}/Dockerfile
|
||
tags: ${{ steps.tags.outputs.aarch64 }}
|
||
labels: ${{ steps.labels.outputs.aarch64 }}
|
||
build-args: ${{ steps.build_args.outputs.aarch64 }}
|
||
cache-from: type=local,src=/tmp/buildx-cache/aarch64
|
||
cache-to: type=local,dest=/tmp/buildx-cache-new/aarch64
|
||
|
||
- name: 💿 Build Addon - amd64
|
||
if: ${{ steps.information.outputs.amd64 == 'true' }}
|
||
uses: docker/build-push-action@v7
|
||
with:
|
||
context: ${{ matrix.addon }}
|
||
push: false
|
||
load: true
|
||
file: ${{ matrix.addon }}/Dockerfile
|
||
tags: ${{ steps.tags.outputs.amd64 }}
|
||
labels: ${{ steps.labels.outputs.amd64 }}
|
||
build-args: ${{ steps.build_args.outputs.amd64 }}
|
||
cache-from: type=local,src=/tmp/buildx-cache/amd64
|
||
cache-to: type=local,dest=/tmp/buildx-cache-new/amd64
|
||
|
||
- name: 💿 Build Addon - i386
|
||
if: ${{ steps.information.outputs.i386 == 'true' }}
|
||
uses: docker/build-push-action@v7
|
||
with:
|
||
context: ${{ matrix.addon }}
|
||
push: false
|
||
load: true
|
||
file: ${{ matrix.addon }}/Dockerfile
|
||
tags: ${{ steps.tags.outputs.i386 }}
|
||
labels: ${{ steps.labels.outputs.i386 }}
|
||
build-args: ${{ steps.build_args.outputs.i386 }}
|
||
cache-from: type=local,src=/tmp/buildx-cache/i386
|
||
cache-to: type=local,dest=/tmp/buildx-cache-new/i386
|
||
|
||
- name: 🗄️ Update cache Folder
|
||
run: |
|
||
rm -rf /tmp/buildx-cache
|
||
mv /tmp/buildx-cache-new /tmp/buildx-cache
|