Skip to content
Merged
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
11 changes: 11 additions & 0 deletions .github/dependabot.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
version: 2
updates:
- package-ecosystem: github-actions
directory: /
schedule:
interval: weekly

- package-ecosystem: docker
directory: /unbound
schedule:
interval: weekly
59 changes: 59 additions & 0 deletions .github/scripts/package-update.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
#!/usr/bin/env bash
# Compares the Alpine packages of a freshly built image with unbound/apk-packages.txt.
# On a difference it updates the list, bumps the patch version and adds a changelog entry.
# Usage (from the repository root): .github/scripts/package-update.sh IMAGE
# Writes changed=true|false and version=X.Y.Z to $GITHUB_OUTPUT when that variable is set.
set -euo pipefail

image=${1:?usage: .github/scripts/package-update.sh IMAGE}
lock=unbound/apk-packages.txt
config=unbound/config.yaml
changelog=unbound/CHANGELOG.md

output() {
if [ -n "${GITHUB_OUTPUT:-}" ]; then
echo "$1" >>"$GITHUB_OUTPUT"
fi
}

current=$(docker run --rm --entrypoint sh "$image" -c 'apk info -v 2>/dev/null | sort')
[ -n "$current" ] || { echo "error: no packages reported by $image" >&2; exit 1; }
previous=$(cat "$lock")

if [ "$current" = "$previous" ]; then
echo "No package changes."
output changed=false
exit 0
fi

version=$(sed -nE 's/^version: "([0-9]+\.[0-9]+\.[0-9]+)"$/\1/p' "$config")
[ -n "$version" ] || { echo "error: cannot read version from $config" >&2; exit 1; }
IFS=. read -r major minor patch <<<"$version"
next="$major.$minor.$((patch + 1))"

[ "$(head -n 1 "$changelog")" = "# Changelog" ] || { echo "error: unexpected header in $changelog" >&2; exit 1; }

changes=$(awk '
function pkg_name(s, n) { n = s; sub(/-[^-]+-r[0-9]+$/, "", n); return n }
NR == FNR { n = pkg_name($0); old[n] = substr($0, length(n) + 2); next }
{ n = pkg_name($0); new[n] = substr($0, length(n) + 2) }
END {
for (n in new) {
if (!(n in old)) printf " - `%s` %s (added)\n", n, new[n]
else if (old[n] != new[n]) printf " - `%s` %s → %s\n", n, old[n], new[n]
}
for (n in old) if (!(n in new)) printf " - `%s` %s (removed)\n", n, old[n]
}' <(printf '%s\n' "$previous") <(printf '%s\n' "$current") | sort)

printf '%s\n' "$current" >"$lock"
sed -i -E "s/^version: \"$version\"$/version: \"$next\"/" "$config"
{
printf '# Changelog\n\n## %s\n\n- Rebuilt with updated Alpine packages:\n%s\n\n' "$next" "$changes"
tail -n +3 "$changelog"
} >"$changelog.tmp"
mv "$changelog.tmp" "$changelog"

echo "Package changes detected, prepared $next:"
printf '%s\n' "$changes"
output changed=true
output "version=$next"
2 changes: 1 addition & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ jobs:
path: "./unbound"
- name: ShellCheck
run: |
docker run --rm -v "$PWD:/mnt:ro" -w /mnt koalaman/shellcheck:v0.11.0 unbound/run.sh tests/smoke.sh
docker run --rm -v "$PWD:/mnt:ro" -w /mnt koalaman/shellcheck:v0.11.0 unbound/run.sh tests/smoke.sh .github/scripts/package-update.sh

build:
runs-on: ubuntu-latest
Expand Down
50 changes: 50 additions & 0 deletions .github/workflows/package-updates.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
name: Package updates

on:
schedule:
- cron: "17 4 * * 1"
workflow_dispatch:

permissions:
contents: write

concurrency:
group: package-updates
cancel-in-progress: false

jobs:
update:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7.0.1

- name: Build image with current Alpine packages
run: docker build --pull --no-cache -t unbound-pkg:ci unbound

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Verify aarch64 before publishing the package release

On the ubuntu-latest runner, this plain docker build only builds the native amd64 image. Unlike .github/workflows/ci.yml, which explicitly builds both linux/amd64 and linux/arm64, this workflow then commits the release with GITHUB_TOKEN, and its own comment notes that this push will not trigger CI. Consequently, an Alpine package update that fails to build or run on aarch64 can be published automatically to supported aarch64 users without that architecture ever being checked; build the arm64 target before the commit as well.

Useful? React with 👍 / 👎.


- name: Compare package set
id: pkg
run: .github/scripts/package-update.sh unbound-pkg:ci

# Pushes made with GITHUB_TOKEN do not trigger the CI workflow, so the release is verified here.
- name: Smoke test
if: steps.pkg.outputs.changed == 'true'
run: |
command -v dig >/dev/null || { sudo apt-get update -qq && sudo apt-get install -y -qq dnsutils; }
tests/smoke.sh unbound-pkg:ci --hardened

- name: Home Assistant app linter
if: steps.pkg.outputs.changed == 'true'
uses: frenck/action-addon-linter@v2.21.1
with:
path: "./unbound"

- name: Commit release
if: steps.pkg.outputs.changed == 'true'
env:
VERSION: ${{ steps.pkg.outputs.version }}
run: |
git config user.name "github-actions[bot]"
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
git add unbound/apk-packages.txt unbound/config.yaml unbound/CHANGELOG.md
git commit -m "Release ${VERSION} with updated Alpine packages"
git push origin "HEAD:${GITHUB_REF_NAME}"
11 changes: 11 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,17 @@ Supports `amd64` and `aarch64`. See the [documentation](./unbound/DOCS.md).
2. Add `https://github.com/TimInTech/ha-addon-unbound`
3. Install **Unbound**, start it, then set `127.0.0.1#5335` as the only upstream DNS server in Pi-hole.

## Maintenance

- **CI** (`ci.yml`): app linter, ShellCheck, amd64/aarch64 builds and `tests/smoke.sh --hardened`
(resolution, DNSSEC, restart, unprivileged start) on every push to `main` and every pull request.
- **Package updates** (`package-updates.yml`, Mondays and on demand): rebuilds the image, compares the installed
Alpine packages with `unbound/apk-packages.txt` and, after the smoke test, commits a patch release with changelog.
- **Dependabot**: weekly pull requests for the Alpine base image and pinned GitHub Actions. A new Alpine branch
changes the package set, so the next package-update run releases it.
- GitHub pauses scheduled workflows after 60 days without repository activity; re-enable *Package updates*
in the Actions tab if that happens.

## License

MIT
7 changes: 6 additions & 1 deletion unbound/DOCS.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,8 @@ It resolves names directly from the root servers, so no third-party DNS provider
- The DNSSEC trust anchor ships with the image (`/var/lib/unbound/root.key`), is refreshed with `unbound-anchor`
on every start and follows key rollovers (RFC 5011) while running. The app data folder is not used; after an
update or reinstall the anchor is bootstrapped again from the image.
- There are no options. Start the app and point your DNS server at it.
- There are no options, by design: the Supervisor writes `options.json` readable for root only, and this app
never runs as root. Start the app and point your DNS server at it.

## Use with Pi-hole

Expand All @@ -37,3 +38,7 @@ dig @192.168.178.56 example.com # expected: NOERROR with an answer

The image is built on your device from `alpine:3.24`. A new app version triggers a rebuild, which also pulls
the current Unbound package of that Alpine branch.

A weekly job rebuilds the image; when any Alpine package in it changed (for example `unbound` or `libssl3`),
it publishes a patch version after the smoke test passed, so Home Assistant offers the update.
`apk-packages.txt` lists the package versions of the latest release.
24 changes: 24 additions & 0 deletions unbound/apk-packages.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
alpine-baselayout-3.7.2-r1
alpine-baselayout-data-3.7.2-r1
alpine-keys-2.6-r0
alpine-release-3.24.1-r0
apk-tools-3.0.6-r0
busybox-1.37.0-r31
busybox-binsh-1.37.0-r31
ca-certificates-bundle-20260611-r0
dnssec-root-20250524-r0
hiredis-1.3.0-r0
libapk-3.0.6-r0
libcrypto3-3.5.7-r0
libevent-2.1.13-r0
libexpat-2.8.4-r0
libssl3-3.5.7-r0
musl-1.2.6-r2
musl-utils-1.2.6-r2
nghttp2-libs-1.69.0-r0
protobuf-c-1.5.2-r2
scanelf-1.3.9-r1
ssl_client-1.37.0-r31
unbound-1.25.2-r1
unbound-libs-1.25.2-r1
zlib-1.3.2-r0