Files
hassio-addons/.github/workflows/onpr_check-pr.yaml
Alexandre bfe91cbeac refactor(webtop,webtop_kde,claude_desktop): share the Selkies startup scripts (#2920)
* 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>
2026-07-28 14:23:52 +02:00

287 lines
12 KiB
YAML
Raw Permalink Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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