From 84cc4bd650759ebae78f3c55fcf2b3b201606131 Mon Sep 17 00:00:00 2001 From: Ed Geraghty Date: Tue, 23 Jun 2026 10:19:36 +0100 Subject: [PATCH 01/16] Extract build-image and use in smoke test --- Makefile | 20 +++++++++++++++----- 1 file changed, 15 insertions(+), 5 deletions(-) diff --git a/Makefile b/Makefile index 4a3a0b7..2b325bd 100644 --- a/Makefile +++ b/Makefile @@ -7,14 +7,20 @@ DEFAULT_SMOKE_TEST_PORT := 8008 .DEFAULT_GOAL := help -.PHONY: help check-envsubst render-fly-config render-azure-container-apps smoke-test +.PHONY: help check-docker check-envsubst render-fly-config render-azure-container-apps smoke-test help: @printf '%s\n' \ 'Available targets:' \ ' make render-fly-config Render fly.toml from templates/fly.template.toml' \ ' make render-azure-container-apps Render azure-container-apps.yaml from templates/azure-container-apps.template.yaml' \ - ' make smoke-test Build the image and run local smoke tests' + ' make smoke-test Build the image, run a container, and execute local smoke tests' + +check-docker: + @command -v docker >/dev/null || { \ + echo 'docker is required for image builds and containerised linters.'; \ + exit 1; \ + } check-envsubst: @command -v envsubst >/dev/null || { \ @@ -22,6 +28,12 @@ check-envsubst: exit 1; \ } +build-image: check-docker + @set -euo pipefail; \ + image="$${SMOKE_TEST_IMAGE:-$(DEFAULT_SMOKE_TEST_IMAGE)}"; \ + echo "Building Docker image: $${image}"; \ + docker build -t "$${image}" . + render-fly-config: check-envsubst @: $${FLY_APP:?Set FLY_APP} @: $${PUBLIC_SERVER_URL:?Set PUBLIC_SERVER_URL} @@ -40,7 +52,7 @@ render-azure-container-apps: check-envsubst @envsubst < templates/azure-container-apps.template.yaml > azure-container-apps.yaml @printf '%s\n' 'Wrote azure-container-apps.yaml' -smoke-test: +smoke-test: build-image @set -euo pipefail; \ image="$${SMOKE_TEST_IMAGE:-$(DEFAULT_SMOKE_TEST_IMAGE)}"; \ container="$${SMOKE_TEST_CONTAINER:-$(DEFAULT_SMOKE_TEST_CONTAINER)}"; \ @@ -55,8 +67,6 @@ smoke-test: }; \ trap cleanup EXIT; \ docker rm -f "$${container}" >/dev/null 2>&1 || true; \ - echo "Building Docker image: $${image}"; \ - docker build -t "$${image}" .; \ echo "Starting container: $${container}"; \ docker run -d --name "$${container}" \ -p "$${host}:$${port}:8008" \ From 6f9620572974c3225ccec873308c6c72fcc41ed6 Mon Sep 17 00:00:00 2001 From: Ed Geraghty Date: Thu, 2 Jul 2026 13:13:27 +0100 Subject: [PATCH 02/16] Make more makefile targets And reuse them in our workflows --- .github/workflows/hadolint.yml | 10 +- .../workflows/headscale-config-checker.yml | 90 ++------------- Makefile | 108 +++++++++++++++++- 3 files changed, 115 insertions(+), 93 deletions(-) diff --git a/.github/workflows/hadolint.yml b/.github/workflows/hadolint.yml index 23402cf..de00b60 100644 --- a/.github/workflows/hadolint.yml +++ b/.github/workflows/hadolint.yml @@ -32,13 +32,9 @@ jobs: - name: Checkout code uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 - - name: Run hadolint - uses: hadolint/hadolint-action@2332a7b74a6de0dda2e2221d575162eba76ba5e5 # v3.3.0 - with: - dockerfile: ./Dockerfile - format: sarif - output-file: hadolint-results.sarif - no-fail: true + - name: Run hadolint via Makefile + continue-on-error: true + run: make lint-docker HADOLINT_FORMAT=sarif HADOLINT_OUTPUT_FILE=hadolint-results.sarif - name: Upload analysis results to GitHub uses: github/codeql-action/upload-sarif@8aad20d150bbac5944a9f9d289da16a4b0d87c1e # v4.36.2 diff --git a/.github/workflows/headscale-config-checker.yml b/.github/workflows/headscale-config-checker.yml index afc68aa..1851747 100644 --- a/.github/workflows/headscale-config-checker.yml +++ b/.github/workflows/headscale-config-checker.yml @@ -17,91 +17,13 @@ jobs: steps: - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 - - name: Get version and download upstream config - run: | - VERSION=$(grep -oP 'HEADSCALE_VERSION="\K[^"]*' Dockerfile) - echo "Found version: $VERSION" - - curl -f -o upstream-config.yaml \ - "https://raw.githubusercontent.com/juanfont/headscale/refs/tags/v${VERSION}/config-example.yaml" - - echo "Successfully downloaded upstream config" - - - name: Generate config from template with defaults - run: | - # Source the defaults - source scripts/defaults.sh - source scripts/ci-defaults.sh - - # Generate config - envsubst < templates/headscale.template.yaml > generated-config.yaml - - - name: Check for new options + - name: Check for new options via Makefile id: check - run: | - # Simple approach: extract keys and apply ignores - extract_keys() { - grep -E "^[[:space:]]*[^#\-].*:" "$1" | \ - sed 's/:[[:space:]]*.*$//' | \ - sed 's/^[[:space:]]*//' | \ - sort -u - } - - # Get list of keys to ignore from DIFF_IGNORE comments (including commented lines) - get_ignored_keys() { - grep "# DIFF_IGNORE" "templates/headscale.template.yaml" | \ - sed -E 's/^[[:space:]]*#?[[:space:]]*//' | \ - sed -E 's/:.*# DIFF_IGNORE.*$//' | \ - sort -u - } - - echo "=== Getting ignored keys ===" - get_ignored_keys > ignored_keys.txt - echo "Keys to ignore:" - cat ignored_keys.txt - echo "=== End ignored keys ===" - - # Extract all keys - extract_keys "generated-config.yaml" > local_all_keys.txt - extract_keys "upstream-config.yaml" > upstream_all_keys.txt - - # Normalize keys (strip optional leading '#' and surrounding spaces) and sort-unique - sed -E 's/^[[:space:]]*#?[[:space:]]*//' upstream_all_keys.txt | sed 's/[[:space:]]*$//' | sort -u > upstream_all_keys_norm.txt - sed -E 's/^[[:space:]]*#?[[:space:]]*//' local_all_keys.txt | sed 's/[[:space:]]*$//' | sort -u > local_all_keys_norm.txt - - # Remove ignored keys from upstream (operate on normalized list) - cp upstream_all_keys_norm.txt upstream_filtered_keys.txt - while IFS= read -r ignore_key; do - if [ -n "$ignore_key" ]; then - grep -v "^${ignore_key}$" upstream_filtered_keys.txt > temp_filtered.txt - mv temp_filtered.txt upstream_filtered_keys.txt - fi - done < ignored_keys.txt - - # Ensure upstream filtered file is sorted/unique for comm - sort -u upstream_filtered_keys.txt -o upstream_filtered_keys.txt - - # Find missing keys using normalized local list - comm -23 upstream_filtered_keys.txt local_all_keys_norm.txt > new-options.txt - - echo "Final comparison:" - echo "Local keys: $(wc -l < local_all_keys_norm.txt)" - echo "Upstream filtered keys: $(wc -l < upstream_filtered_keys.txt)" - - if [ -s new-options.txt ]; then - echo "has_missing=true" >> $GITHUB_OUTPUT - echo "🆕 New configuration keys found:" - cat new-options.txt - else - echo "has_missing=false" >> $GITHUB_OUTPUT - echo "✅ No new configuration keys found" - fi - - # Cleanup - rm -f ignored_keys.txt local_all_keys.txt upstream_all_keys.txt upstream_filtered_keys.txt temp_filtered.txt upstream_all_keys_norm.txt local_all_keys_norm.txt + continue-on-error: true + run: make check-config-drift - name: Comment on PR - if: github.event_name == 'pull_request' && steps.check.outputs.has_missing == 'true' + if: github.event_name == 'pull_request' && steps.check.outcome == 'failure' uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 with: script: | @@ -114,3 +36,7 @@ jobs: issue_number: context.issue.number, body: `## 🆕 New Headscale Config Options\n\n\`\`\`diff\n${newOptions.split('\n').map(line => line.trim() ? `+ ${line}` : '').filter(line => line).join('\n')}\`\`\`` }); + + - name: Fail when new options are found + if: steps.check.outcome == 'failure' + run: exit 1 diff --git a/Makefile b/Makefile index 2b325bd..81141f2 100644 --- a/Makefile +++ b/Makefile @@ -4,17 +4,32 @@ DEFAULT_SMOKE_TEST_IMAGE := headscale:latest DEFAULT_SMOKE_TEST_CONTAINER := headscale-container DEFAULT_SMOKE_TEST_HOST := 127.0.0.1 DEFAULT_SMOKE_TEST_PORT := 8008 +DEFAULT_HADOLINT_IMAGE := hadolint/hadolint:v2.12.0-alpine +DEFAULT_SHELLCHECK_IMAGE := koalaman/shellcheck:v0.10.0 .DEFAULT_GOAL := help -.PHONY: help check-docker check-envsubst render-fly-config render-azure-container-apps smoke-test +.PHONY: help check-curl check-docker check-envsubst build lint-docker lint-shell render-fly-config render-azure-container-apps render-headscale-config check-config-drift check clean smoke-test help: @printf '%s\n' \ 'Available targets:' \ + ' make build Build the Docker image locally' \ + ' make lint-docker Lint Dockerfile with hadolint' \ + ' make lint-shell Lint shell scripts with shellcheck' \ ' make render-fly-config Render fly.toml from templates/fly.template.toml' \ ' make render-azure-container-apps Render azure-container-apps.yaml from templates/azure-container-apps.template.yaml' \ - ' make smoke-test Build the image, run a container, and execute local smoke tests' + ' make render-headscale-config Render generated-config.yaml from templates/headscale.template.yaml' \ + ' make check-config-drift Compare rendered config against upstream Headscale config' \ + ' make check Run the local validation suite' \ + ' make clean Remove generated local artefacts' \ + ' make smoke-test Build the image and run local smoke tests' + +check-curl: + @command -v curl >/dev/null || { \ + echo 'curl is required for upstream config checks.'; \ + exit 1; \ + } check-docker: @command -v docker >/dev/null || { \ @@ -28,12 +43,39 @@ check-envsubst: exit 1; \ } -build-image: check-docker +build: check-docker @set -euo pipefail; \ image="$${SMOKE_TEST_IMAGE:-$(DEFAULT_SMOKE_TEST_IMAGE)}"; \ echo "Building Docker image: $${image}"; \ docker build -t "$${image}" . +lint-docker: check-docker + @set -euo pipefail; \ + image="$${HADOLINT_IMAGE:-$(DEFAULT_HADOLINT_IMAGE)}"; \ + format="$${HADOLINT_FORMAT:-tty}"; \ + output_file="$${HADOLINT_OUTPUT_FILE:-}"; \ + echo "Running hadolint using $${image}"; \ + if [[ -n "$${output_file}" ]]; then \ + docker run --rm -i -v "$${PWD}:/workdir" -w /workdir --entrypoint hadolint "$${image}" \ + --format "$${format}" Dockerfile > "$${output_file}"; \ + printf '%s\n' "Wrote $${output_file}"; \ + else \ + docker run --rm -i -v "$${PWD}:/workdir" -w /workdir --entrypoint hadolint "$${image}" \ + --format "$${format}" Dockerfile; \ + fi + +lint-shell: check-docker + @set -euo pipefail; \ + image="$${SHELLCHECK_IMAGE:-$(DEFAULT_SHELLCHECK_IMAGE)}"; \ + set -- scripts/*.sh; \ + if [[ "$${1}" == 'scripts/*.sh' ]]; then \ + echo 'No shell scripts found under scripts/'; \ + exit 0; \ + fi; \ + echo "Running shellcheck using $${image}"; \ + docker run --rm -i -v "$${PWD}:/workdir" -w /workdir --entrypoint shellcheck "$${image}" \ + --shell=bash "$${@}" + render-fly-config: check-envsubst @: $${FLY_APP:?Set FLY_APP} @: $${PUBLIC_SERVER_URL:?Set PUBLIC_SERVER_URL} @@ -52,7 +94,65 @@ render-azure-container-apps: check-envsubst @envsubst < templates/azure-container-apps.template.yaml > azure-container-apps.yaml @printf '%s\n' 'Wrote azure-container-apps.yaml' -smoke-test: build-image +render-headscale-config: check-envsubst + @set -euo pipefail; \ + source scripts/defaults.sh; \ + source scripts/ci-defaults.sh; \ + envsubst < templates/headscale.template.yaml > generated-config.yaml; \ + printf '%s\n' 'Wrote generated-config.yaml' + +check-config-drift: check-curl render-headscale-config + @set -euo pipefail; \ + version="$$(awk -F'"' '/^ARG HEADSCALE_VERSION=/{ print $$2; exit }' Dockerfile)"; \ + cleanup() { \ + rm -f ignored_keys.txt local_all_keys.txt upstream_all_keys.txt upstream_all_keys_norm.txt local_all_keys_norm.txt upstream_filtered_keys.txt temp_filtered.txt; \ + }; \ + trap cleanup EXIT; \ + echo "Downloading upstream Headscale config for v$${version}"; \ + curl --fail --silent --show-error -o upstream-config.yaml \ + "https://raw.githubusercontent.com/juanfont/headscale/refs/tags/v$${version}/config-example.yaml"; \ + extract_keys() { \ + grep -E '^[[:space:]]*[^#\-].*:' "$$1" | \ + sed 's/:[[:space:]]*.*$$//' | \ + sed 's/^[[:space:]]*//' | \ + sort -u; \ + }; \ + get_ignored_keys() { \ + grep '# DIFF_IGNORE' templates/headscale.template.yaml | \ + sed -E 's/^[[:space:]]*#?[[:space:]]*//' | \ + sed -E 's/:.*# DIFF_IGNORE.*$$//' | \ + sort -u; \ + }; \ + get_ignored_keys > ignored_keys.txt; \ + extract_keys generated-config.yaml > local_all_keys.txt; \ + extract_keys upstream-config.yaml > upstream_all_keys.txt; \ + sed -E 's/^[[:space:]]*#?[[:space:]]*//' upstream_all_keys.txt | sed 's/[[:space:]]*$$//' | sort -u > upstream_all_keys_norm.txt; \ + sed -E 's/^[[:space:]]*#?[[:space:]]*//' local_all_keys.txt | sed 's/[[:space:]]*$$//' | sort -u > local_all_keys_norm.txt; \ + cp upstream_all_keys_norm.txt upstream_filtered_keys.txt; \ + while IFS= read -r ignore_key; do \ + if [[ -n "$${ignore_key}" ]]; then \ + grep -v "^$${ignore_key}$$" upstream_filtered_keys.txt > temp_filtered.txt || true; \ + mv temp_filtered.txt upstream_filtered_keys.txt; \ + fi; \ + done < ignored_keys.txt; \ + sort -u upstream_filtered_keys.txt -o upstream_filtered_keys.txt; \ + comm -23 upstream_filtered_keys.txt local_all_keys_norm.txt > new-options.txt; \ + echo "Local keys: $$(wc -l < local_all_keys_norm.txt)"; \ + echo "Upstream filtered keys: $$(wc -l < upstream_filtered_keys.txt)"; \ + if [[ -s new-options.txt ]]; then \ + echo 'New configuration keys found:'; \ + cat new-options.txt; \ + exit 1; \ + fi; \ + rm -f new-options.txt; \ + echo 'No new configuration keys found' + +check: lint-docker lint-shell check-config-drift + +clean: + @rm -f generated-config.yaml upstream-config.yaml new-options.txt azure-container-apps.yaml fly.toml hadolint-results.sarif + +smoke-test: build @set -euo pipefail; \ image="$${SMOKE_TEST_IMAGE:-$(DEFAULT_SMOKE_TEST_IMAGE)}"; \ container="$${SMOKE_TEST_CONTAINER:-$(DEFAULT_SMOKE_TEST_CONTAINER)}"; \ From 632cd426515633bc9f1300b8117865e759a32778 Mon Sep 17 00:00:00 2001 From: Ed Geraghty Date: Thu, 2 Jul 2026 13:39:21 +0100 Subject: [PATCH 03/16] Fix crash on no `#DIFF_IGNORE` lines Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- Makefile | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Makefile b/Makefile index 81141f2..43e986c 100644 --- a/Makefile +++ b/Makefile @@ -118,11 +118,11 @@ check-config-drift: check-curl render-headscale-config sort -u; \ }; \ get_ignored_keys() { \ - grep '# DIFF_IGNORE' templates/headscale.template.yaml | \ - sed -E 's/^[[:space:]]*#?[[:space:]]*//' | \ - sed -E 's/:.*# DIFF_IGNORE.*$$//' | \ + awk '/# DIFF_IGNORE/ { gsub(/^[[:space:]]*#?[[:space:]]*/, ""); sub(/:.*# DIFF_IGNORE.*/, ""); print }' \ + templates/headscale.template.yaml | \ sort -u; \ }; \ + get_ignored_keys > ignored_keys.txt; \ extract_keys generated-config.yaml > local_all_keys.txt; \ extract_keys upstream-config.yaml > upstream_all_keys.txt; \ From d917c47f0d1dcb2052113b8ab5501580ed1ef018 Mon Sep 17 00:00:00 2001 From: Ed Geraghty Date: Thu, 2 Jul 2026 13:43:33 +0100 Subject: [PATCH 04/16] Make PR comment step only run on `new-options.txt` By adding hashFiles('new-options.txt') != '' to the step condition. Also guarded the github-script body with fs.existsSync(...) so a missing file now cleanly skips the comment instead of throwing a secondary error --- .github/workflows/headscale-config-checker.yml | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/.github/workflows/headscale-config-checker.yml b/.github/workflows/headscale-config-checker.yml index 1851747..8ad44ae 100644 --- a/.github/workflows/headscale-config-checker.yml +++ b/.github/workflows/headscale-config-checker.yml @@ -23,11 +23,16 @@ jobs: run: make check-config-drift - name: Comment on PR - if: github.event_name == 'pull_request' && steps.check.outcome == 'failure' + if: github.event_name == 'pull_request' && steps.check.outcome == 'failure' && hashFiles('new-options.txt') != '' uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 with: script: | const fs = require('fs'); + if (!fs.existsSync('new-options.txt')) { + console.log('Skipping PR comment because new-options.txt was not created.'); + return; + } + const newOptions = fs.readFileSync('new-options.txt', 'utf8'); github.rest.issues.createComment({ From 2187ea7d0d7ee90ebd85950f1d78c128f4a764cc Mon Sep 17 00:00:00 2001 From: Ed Geraghty Date: Thu, 2 Jul 2026 13:46:30 +0100 Subject: [PATCH 05/16] Lint --- .github/workflows/headscale-config-checker.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/headscale-config-checker.yml b/.github/workflows/headscale-config-checker.yml index 8ad44ae..a262d27 100644 --- a/.github/workflows/headscale-config-checker.yml +++ b/.github/workflows/headscale-config-checker.yml @@ -34,7 +34,7 @@ jobs: } const newOptions = fs.readFileSync('new-options.txt', 'utf8'); - + github.rest.issues.createComment({ owner: context.repo.owner, repo: context.repo.repo, From 591937ac5bdeaf712c1beb751060d428f91f632f Mon Sep 17 00:00:00 2001 From: Ed Geraghty Date: Thu, 2 Jul 2026 13:52:30 +0100 Subject: [PATCH 06/16] Remove blank line breaking backslash-multiline --- Makefile | 1 - 1 file changed, 1 deletion(-) diff --git a/Makefile b/Makefile index 43e986c..136b4b9 100644 --- a/Makefile +++ b/Makefile @@ -122,7 +122,6 @@ check-config-drift: check-curl render-headscale-config templates/headscale.template.yaml | \ sort -u; \ }; \ - get_ignored_keys > ignored_keys.txt; \ extract_keys generated-config.yaml > local_all_keys.txt; \ extract_keys upstream-config.yaml > upstream_all_keys.txt; \ From ab7ae35dcd0c986e704413d3265dfc21dbdc98a0 Mon Sep 17 00:00:00 2001 From: Ed Geraghty Date: Thu, 2 Jul 2026 13:53:48 +0100 Subject: [PATCH 07/16] Use `await` for the comment API call --- .github/workflows/headscale-config-checker.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/headscale-config-checker.yml b/.github/workflows/headscale-config-checker.yml index a262d27..43764c0 100644 --- a/.github/workflows/headscale-config-checker.yml +++ b/.github/workflows/headscale-config-checker.yml @@ -35,7 +35,7 @@ jobs: const newOptions = fs.readFileSync('new-options.txt', 'utf8'); - github.rest.issues.createComment({ + await github.rest.issues.createComment({ owner: context.repo.owner, repo: context.repo.repo, issue_number: context.issue.number, From 0c3237a21c462ad1d6dd1a31f0900864b4b31aa1 Mon Sep 17 00:00:00 2001 From: Ed Geraghty Date: Thu, 2 Jul 2026 14:01:42 +0100 Subject: [PATCH 08/16] Fix hadolint findings by amalgamating RUN layers --- Dockerfile | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/Dockerfile b/Dockerfile index fae056e..5c6e230 100644 --- a/Dockerfile +++ b/Dockerfile @@ -52,15 +52,13 @@ FROM node:${HEADSCALE_ADMIN_NODE_VERSION}-alpine AS admin-gui apk add --no-cache --virtual BuildTimeDeps git; WORKDIR /app - RUN git clone ${HEADSCALE_ADMIN_REPO} . && \ - git checkout ${HEADSCALE_ADMIN_VERSION} - ENV ENDPOINT="${HEADSCALE_ADMIN_ENDPOINT}" - RUN npm install && npm run build; - - RUN mv /app/build /app${HEADSCALE_ADMIN_ENDPOINT} - - RUN apk del BuildTimeDeps + RUN git clone ${HEADSCALE_ADMIN_REPO} . && \ + git checkout ${HEADSCALE_ADMIN_VERSION} && \ + npm install && \ + npm run build && \ + mv /app/build /app${HEADSCALE_ADMIN_ENDPOINT} && \ + apk del BuildTimeDeps # Build our main image FROM alpine:${MAIN_IMAGE_ALPINE_VERSION} From 04606844488d063e8a35191d7fa473e8b3f9b145 Mon Sep 17 00:00:00 2001 From: Ed Geraghty Date: Thu, 2 Jul 2026 14:04:18 +0100 Subject: [PATCH 09/16] Ignore unpinned git apk version --- Dockerfile | 1 + 1 file changed, 1 insertion(+) diff --git a/Dockerfile b/Dockerfile index 5c6e230..3eab2b2 100644 --- a/Dockerfile +++ b/Dockerfile @@ -48,6 +48,7 @@ FROM node:${HEADSCALE_ADMIN_NODE_VERSION}-alpine AS admin-gui ARG HEADSCALE_ADMIN_VERSION ARG HEADSCALE_ADMIN_ENDPOINT + # hadolint ignore=DL3018 # Ignore unpinned apk add RUN apk --no-cache upgrade; \ apk add --no-cache --virtual BuildTimeDeps git; From 5f2481e364083a89cbf066d61775910f79eac5d9 Mon Sep 17 00:00:00 2001 From: Ed Geraghty Date: Thu, 2 Jul 2026 14:25:41 +0100 Subject: [PATCH 10/16] Fix ignored keys in config file drift comparator The `yq` extractor now walks YAML paths instead of flattening to leaf keys, which fixes the false negatives from repeated names like enabled --- Makefile | 100 ++++++++++++++++++++++++++++++++++++++++++------------- 1 file changed, 76 insertions(+), 24 deletions(-) diff --git a/Makefile b/Makefile index 136b4b9..66b141a 100644 --- a/Makefile +++ b/Makefile @@ -6,6 +6,7 @@ DEFAULT_SMOKE_TEST_HOST := 127.0.0.1 DEFAULT_SMOKE_TEST_PORT := 8008 DEFAULT_HADOLINT_IMAGE := hadolint/hadolint:v2.12.0-alpine DEFAULT_SHELLCHECK_IMAGE := koalaman/shellcheck:v0.10.0 +DEFAULT_YQ_IMAGE := mikefarah/yq:4.44.3 .DEFAULT_GOAL := help @@ -101,43 +102,94 @@ render-headscale-config: check-envsubst envsubst < templates/headscale.template.yaml > generated-config.yaml; \ printf '%s\n' 'Wrote generated-config.yaml' -check-config-drift: check-curl render-headscale-config +check-config-drift: check-curl check-docker render-headscale-config @set -euo pipefail; \ version="$$(awk -F'"' '/^ARG HEADSCALE_VERSION=/{ print $$2; exit }' Dockerfile)"; \ + yq_image="$${YQ_IMAGE:-$(DEFAULT_YQ_IMAGE)}"; \ cleanup() { \ - rm -f ignored_keys.txt local_all_keys.txt upstream_all_keys.txt upstream_all_keys_norm.txt local_all_keys_norm.txt upstream_filtered_keys.txt temp_filtered.txt; \ + rm -f ignored_paths.txt local_all_paths.txt upstream_all_paths.txt upstream_filtered_paths.txt temp_filtered.txt; \ }; \ trap cleanup EXIT; \ echo "Downloading upstream Headscale config for v$${version}"; \ curl --fail --silent --show-error -o upstream-config.yaml \ "https://raw.githubusercontent.com/juanfont/headscale/refs/tags/v$${version}/config-example.yaml"; \ - extract_keys() { \ - grep -E '^[[:space:]]*[^#\-].*:' "$$1" | \ - sed 's/:[[:space:]]*.*$$//' | \ - sed 's/^[[:space:]]*//' | \ + extract_paths() { \ + docker run --rm -i -v "$${PWD}:/workdir" -w /workdir --entrypoint yq "$${yq_image}" \ + e '.. | path | select(length > 0) | map(select(tag == "!!str")) | select(length > 0) | join(".")' "$$1" | \ sort -u; \ }; \ - get_ignored_keys() { \ - awk '/# DIFF_IGNORE/ { gsub(/^[[:space:]]*#?[[:space:]]*/, ""); sub(/:.*# DIFF_IGNORE.*/, ""); print }' \ - templates/headscale.template.yaml | \ + get_ignored_paths() { \ + awk 'BEGIN { depth = 0; } \ + function get_indent(text) { \ + indent = match(text, /[^ ]/) - 1; \ + return indent < 0 ? 0 : indent; \ + } \ + function get_key(text) { \ + key = text; \ + sub(/^[[:space:]]*#[[:space:]]?/, "", key); \ + sub(/^[[:space:]]*/, "", key); \ + sub(/:.*/, "", key); \ + return key; \ + } \ + function print_path(indent, key, current_depth, current, path_index) { \ + current_depth = depth; \ + while (current_depth > 0 && indent <= indents[current_depth]) { \ + current_depth--; \ + } \ + current = key; \ + if (current_depth > 0) { \ + current = path[1]; \ + for (path_index = 2; path_index <= current_depth; path_index++) { \ + current = current "." path[path_index]; \ + } \ + current = current "." key; \ + } \ + print current; \ + } \ + { \ + line = $$0; \ + if (line ~ /# DIFF_IGNORE/ && line ~ /^[[:space:]]*#/) { \ + candidate = line; \ + sub(/[[:space:]]+# DIFF_IGNORE[[:space:]]*$$/, "", candidate); \ + sub(/#/, "", candidate); \ + if (candidate !~ /^[[:space:]]*[[:alnum:]_.-]+[[:space:]]*:[[:space:]]*([^#].*)?$$/) { \ + next; \ + } \ + print_path(get_indent(candidate), get_key(candidate)); \ + next; \ + } \ + if (line ~ /^[[:space:]]*[[:alnum:]_.-]+[[:space:]]*:[[:space:]]*([^#].*)?$$/) { \ + indent = get_indent(line); \ + key = get_key(line); \ + while (depth > 0 && indent <= indents[depth]) { \ + delete path[depth]; \ + delete indents[depth]; \ + depth--; \ + } \ + depth++; \ + path[depth] = key; \ + indents[depth] = indent; \ + if (line ~ /# DIFF_IGNORE/) { \ + print_path(indent, key); \ + } \ + } \ + }' templates/headscale.template.yaml | \ sort -u; \ }; \ - get_ignored_keys > ignored_keys.txt; \ - extract_keys generated-config.yaml > local_all_keys.txt; \ - extract_keys upstream-config.yaml > upstream_all_keys.txt; \ - sed -E 's/^[[:space:]]*#?[[:space:]]*//' upstream_all_keys.txt | sed 's/[[:space:]]*$$//' | sort -u > upstream_all_keys_norm.txt; \ - sed -E 's/^[[:space:]]*#?[[:space:]]*//' local_all_keys.txt | sed 's/[[:space:]]*$$//' | sort -u > local_all_keys_norm.txt; \ - cp upstream_all_keys_norm.txt upstream_filtered_keys.txt; \ - while IFS= read -r ignore_key; do \ - if [[ -n "$${ignore_key}" ]]; then \ - grep -v "^$${ignore_key}$$" upstream_filtered_keys.txt > temp_filtered.txt || true; \ - mv temp_filtered.txt upstream_filtered_keys.txt; \ + get_ignored_paths > ignored_paths.txt; \ + extract_paths generated-config.yaml > local_all_paths.txt; \ + extract_paths upstream-config.yaml > upstream_all_paths.txt; \ + cp upstream_all_paths.txt upstream_filtered_paths.txt; \ + while IFS= read -r ignore_path; do \ + if [[ -n "$${ignore_path}" ]]; then \ + grep -F -x -v "$${ignore_path}" upstream_filtered_paths.txt > temp_filtered.txt || true; \ + mv temp_filtered.txt upstream_filtered_paths.txt; \ fi; \ - done < ignored_keys.txt; \ - sort -u upstream_filtered_keys.txt -o upstream_filtered_keys.txt; \ - comm -23 upstream_filtered_keys.txt local_all_keys_norm.txt > new-options.txt; \ - echo "Local keys: $$(wc -l < local_all_keys_norm.txt)"; \ - echo "Upstream filtered keys: $$(wc -l < upstream_filtered_keys.txt)"; \ + done < ignored_paths.txt; \ + sort -u upstream_filtered_paths.txt -o upstream_filtered_paths.txt; \ + comm -23 upstream_filtered_paths.txt local_all_paths.txt > new-options.txt; \ + echo "Local paths: $$(wc -l < local_all_paths.txt)"; \ + echo "Upstream filtered paths: $$(wc -l < upstream_filtered_paths.txt)"; \ if [[ -s new-options.txt ]]; then \ echo 'New configuration keys found:'; \ cat new-options.txt; \ From ec3d4307902997f910e444a3ddcb81d77f9170f9 Mon Sep 17 00:00:00 2001 From: Ed Geraghty Date: Thu, 2 Jul 2026 14:27:54 +0100 Subject: [PATCH 11/16] Use shallow clone and path quotes Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- Dockerfile | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/Dockerfile b/Dockerfile index 3eab2b2..8178dba 100644 --- a/Dockerfile +++ b/Dockerfile @@ -54,11 +54,10 @@ FROM node:${HEADSCALE_ADMIN_NODE_VERSION}-alpine AS admin-gui WORKDIR /app ENV ENDPOINT="${HEADSCALE_ADMIN_ENDPOINT}" - RUN git clone ${HEADSCALE_ADMIN_REPO} . && \ - git checkout ${HEADSCALE_ADMIN_VERSION} && \ + RUN git clone --depth 1 --branch "${HEADSCALE_ADMIN_VERSION}" "${HEADSCALE_ADMIN_REPO}" . && \ npm install && \ npm run build && \ - mv /app/build /app${HEADSCALE_ADMIN_ENDPOINT} && \ + mv /app/build "/app${HEADSCALE_ADMIN_ENDPOINT}" && \ apk del BuildTimeDeps # Build our main image From cb840795d2bff3496f3a568c702231088682612b Mon Sep 17 00:00:00 2001 From: Ed Geraghty Date: Thu, 2 Jul 2026 14:30:48 +0100 Subject: [PATCH 12/16] Make drift failure more obvious, surface errors Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- .github/workflows/headscale-config-checker.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/headscale-config-checker.yml b/.github/workflows/headscale-config-checker.yml index 43764c0..8d12bbd 100644 --- a/.github/workflows/headscale-config-checker.yml +++ b/.github/workflows/headscale-config-checker.yml @@ -42,6 +42,6 @@ jobs: body: `## 🆕 New Headscale Config Options\n\n\`\`\`diff\n${newOptions.split('\n').map(line => line.trim() ? `+ ${line}` : '').filter(line => line).join('\n')}\`\`\`` }); - - name: Fail when new options are found + - name: Fail when drift check fails if: steps.check.outcome == 'failure' - run: exit 1 + run: make check-config-drift From 791c9d2ea6b7174f5d0d52c925e2588cd97cd1cc Mon Sep 17 00:00:00 2001 From: Ed Geraghty Date: Thu, 2 Jul 2026 14:38:28 +0100 Subject: [PATCH 13/16] Remove whitespace in multiline RUN --- Dockerfile | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Dockerfile b/Dockerfile index 8178dba..12f5ba1 100644 --- a/Dockerfile +++ b/Dockerfile @@ -54,10 +54,10 @@ FROM node:${HEADSCALE_ADMIN_NODE_VERSION}-alpine AS admin-gui WORKDIR /app ENV ENDPOINT="${HEADSCALE_ADMIN_ENDPOINT}" - RUN git clone --depth 1 --branch "${HEADSCALE_ADMIN_VERSION}" "${HEADSCALE_ADMIN_REPO}" . && \ + RUN git clone --depth 1 --branch "${HEADSCALE_ADMIN_VERSION}" "${HEADSCALE_ADMIN_REPO}" . && \ npm install && \ npm run build && \ - mv /app/build "/app${HEADSCALE_ADMIN_ENDPOINT}" && \ + mv /app/build "/app${HEADSCALE_ADMIN_ENDPOINT}" && \ apk del BuildTimeDeps # Build our main image From 0079fb527d854533f330433d686dedbd9a59b303 Mon Sep 17 00:00:00 2001 From: Ed Geraghty Date: Thu, 2 Jul 2026 14:43:24 +0100 Subject: [PATCH 14/16] Fix line endings to LF --- Dockerfile | 310 ++++++++++++++++++++++++++--------------------------- 1 file changed, 155 insertions(+), 155 deletions(-) diff --git a/Dockerfile b/Dockerfile index 12f5ba1..8071904 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,155 +1,155 @@ -################### -# BUILD PREP -################### -# Tool version arguments -# Bump these every time there is a new release. -# We're pulling these from github source, don't forget to bump the checksum! -ARG HEADSCALE_VERSION="0.29.2" -ARG HEADSCALE_SHA256="858ef94bca9ecdfc742c24119c831e437e1b75691fdd5041be4059db1a38aac0" - -ARG LITESTREAM_VERSION="0.5.13" -ARG LITESTREAM_SHA256="fc3420fea7d2f92d4d604aceeb0d7c63dc2c91f6ee5c1547cc05e25629e70f9f" - -# We're building these from source, so we need to specify the versions here rather than hash -ARG HEADSCALE_ADMIN_ENDPOINT="/admin" -ARG HEADSCALE_ADMIN_REPO="https://github.com/privacyint/headscale-admin" -ARG HEADSCALE_ADMIN_VERSION="v0.28.0" -ARG HEADSCALE_ADMIN_NODE_VERSION="25" - -# No checksum needed for these tools, we pull from official images -ARG CADDY_VERSION="2.11.4" -ARG MAIN_IMAGE_ALPINE_VERSION="3.24.1" - -# github download links -# These should never need adjusting unless the URIs change -ARG HEADSCALE_DOWNLOAD_URL="https://github.com/juanfont/headscale/releases/download/v${HEADSCALE_VERSION}/headscale_${HEADSCALE_VERSION}_linux_amd64" -ARG LITESTREAM_DOWNLOAD_URL="https://github.com/benbjohnson/litestream/releases/download/v${LITESTREAM_VERSION}/litestream-${LITESTREAM_VERSION}-linux-x86_64.tar.gz" - -################### -# BUILD PROCESS -################### - -# Build caddy with Cloudflare DNS support -FROM caddy:${CADDY_VERSION}-builder AS caddy-builder - # Set SHELL flags for RUN commands to allow -e and pipefail - # Rationale: https://github.com/hadolint/hadolint/wiki/DL4006 - SHELL ["/bin/ash", "-eo", "pipefail", "-c"] - - RUN xcaddy build \ - --with github.com/caddy-dns/cloudflare - -# Build the admin GUI from source -FROM node:${HEADSCALE_ADMIN_NODE_VERSION}-alpine AS admin-gui - # Set SHELL flags for RUN commands to allow -e and pipefail - # Rationale: https://github.com/hadolint/hadolint/wiki/DL4006 - SHELL ["/bin/ash", "-eo", "pipefail", "-c"] - - ARG HEADSCALE_ADMIN_REPO - ARG HEADSCALE_ADMIN_VERSION - ARG HEADSCALE_ADMIN_ENDPOINT - - # hadolint ignore=DL3018 # Ignore unpinned apk add - RUN apk --no-cache upgrade; \ - apk add --no-cache --virtual BuildTimeDeps git; - - WORKDIR /app - ENV ENDPOINT="${HEADSCALE_ADMIN_ENDPOINT}" - RUN git clone --depth 1 --branch "${HEADSCALE_ADMIN_VERSION}" "${HEADSCALE_ADMIN_REPO}" . && \ - npm install && \ - npm run build && \ - mv /app/build "/app${HEADSCALE_ADMIN_ENDPOINT}" && \ - apk del BuildTimeDeps - -# Build our main image -FROM alpine:${MAIN_IMAGE_ALPINE_VERSION} - # Set SHELL flags for RUN commands to allow -e and pipefail - # Rationale: https://github.com/hadolint/hadolint/wiki/DL4006 - SHELL ["/bin/ash", "-eo", "pipefail", "-c"] - - # Import our "global" `ARG` values into this stage - ARG HEADSCALE_DOWNLOAD_URL - ARG HEADSCALE_SHA256 - ARG LITESTREAM_DOWNLOAD_URL - ARG LITESTREAM_SHA256 - - # Upgrade system and install various dependencies - # - BusyBox's wget isn't reliable enough - # - I'm gonna need a better shell - # - gettext provides `envsubst` for templating - # hadolint ignore=DL3018,SC2086 - RUN BUILD_DEPS="wget"; \ - RUNTIME_DEPS="bash gettext"; \ - apk --no-cache upgrade; \ - apk add --no-cache --virtual BuildTimeDeps ${BUILD_DEPS}; \ - apk add --no-cache ${RUNTIME_DEPS} - - # Copy caddy from the first stage - COPY --from=caddy-builder /usr/bin/caddy /usr/local/bin/caddy - # Caddy smoke test - RUN [ "$(command -v caddy)" = '/usr/local/bin/caddy' ]; \ - caddy version - - # Headscale - RUN set -ex; { \ - wget --retry-connrefused \ - --waitretry=1 \ - --read-timeout=20 \ - --timeout=15 \ - -t 0 \ - -q \ - -O headscale \ - ${HEADSCALE_DOWNLOAD_URL} || { \ - echo "Failed to download Headscale from ${HEADSCALE_DOWNLOAD_URL}"; \ - exit 1; \ - }; \ - echo "${HEADSCALE_SHA256} *headscale" | sha256sum -c - >/dev/null 2>&1; \ - chmod +x headscale; \ - mv headscale /usr/local/bin/; \ - }; \ - # Headscale smoke test - [ "$(command -v headscale)" = '/usr/local/bin/headscale' ]; \ - headscale version; - - # Litestream - RUN set -ex; { \ - wget --retry-connrefused \ - --waitretry=1 \ - --read-timeout=20 \ - --timeout=15 \ - -t 0 \ - -q \ - -O litestream.tar.gz \ - ${LITESTREAM_DOWNLOAD_URL} \ - ; \ - echo "${LITESTREAM_SHA256} *litestream.tar.gz" | sha256sum -c - >/dev/null 2>&1; \ - tar -xf litestream.tar.gz; \ - mv litestream /usr/local/bin/; \ - rm -f litestream.tar.gz; \ - }; \ - # Litestream smoke test - [ "$(command -v litestream)" = '/usr/local/bin/litestream' ]; \ - litestream version; - - # Headscale web GUI - COPY --from=admin-gui /app/admin/ /admin-gui/admin/ - - # Remove build-time dependencies - RUN apk del BuildTimeDeps - - # Copy configuration templates - COPY ./templates/headscale.template.yaml /etc/headscale/config.yaml - COPY ./templates/litestream.template.yml /etc/litestream.yml - COPY ./templates/Caddyfile-http.template /etc/caddy/Caddyfile-http - COPY ./templates/Caddyfile-https.template /etc/caddy/Caddyfile-https - - # Copy and setup scripts into a safe bin directory - COPY --chmod=755 ./scripts/ /usr/local/bin/ - - # Default HTTPS port - override with $PUBLIC_LISTEN_PORT environment variable - EXPOSE 443 - - # Health check to ensure services are running - HEALTHCHECK --interval=30s --timeout=10s --start-period=30s --retries=3 \ - CMD headscale version && caddy version || exit 1 - - ENTRYPOINT ["/usr/local/bin/container-entrypoint.sh"] +################### +# BUILD PREP +################### +# Tool version arguments +# Bump these every time there is a new release. +# We're pulling these from github source, don't forget to bump the checksum! +ARG HEADSCALE_VERSION="0.29.2" +ARG HEADSCALE_SHA256="858ef94bca9ecdfc742c24119c831e437e1b75691fdd5041be4059db1a38aac0" + +ARG LITESTREAM_VERSION="0.5.13" +ARG LITESTREAM_SHA256="fc3420fea7d2f92d4d604aceeb0d7c63dc2c91f6ee5c1547cc05e25629e70f9f" + +# We're building these from source, so we need to specify the versions here rather than hash +ARG HEADSCALE_ADMIN_ENDPOINT="/admin" +ARG HEADSCALE_ADMIN_REPO="https://github.com/privacyint/headscale-admin" +ARG HEADSCALE_ADMIN_VERSION="v0.28.0" +ARG HEADSCALE_ADMIN_NODE_VERSION="25" + +# No checksum needed for these tools, we pull from official images +ARG CADDY_VERSION="2.11.4" +ARG MAIN_IMAGE_ALPINE_VERSION="3.24.1" + +# github download links +# These should never need adjusting unless the URIs change +ARG HEADSCALE_DOWNLOAD_URL="https://github.com/juanfont/headscale/releases/download/v${HEADSCALE_VERSION}/headscale_${HEADSCALE_VERSION}_linux_amd64" +ARG LITESTREAM_DOWNLOAD_URL="https://github.com/benbjohnson/litestream/releases/download/v${LITESTREAM_VERSION}/litestream-${LITESTREAM_VERSION}-linux-x86_64.tar.gz" + +################### +# BUILD PROCESS +################### + +# Build caddy with Cloudflare DNS support +FROM caddy:${CADDY_VERSION}-builder AS caddy-builder + # Set SHELL flags for RUN commands to allow -e and pipefail + # Rationale: https://github.com/hadolint/hadolint/wiki/DL4006 + SHELL ["/bin/ash", "-eo", "pipefail", "-c"] + + RUN xcaddy build \ + --with github.com/caddy-dns/cloudflare + +# Build the admin GUI from source +FROM node:${HEADSCALE_ADMIN_NODE_VERSION}-alpine AS admin-gui + # Set SHELL flags for RUN commands to allow -e and pipefail + # Rationale: https://github.com/hadolint/hadolint/wiki/DL4006 + SHELL ["/bin/ash", "-eo", "pipefail", "-c"] + + ARG HEADSCALE_ADMIN_REPO + ARG HEADSCALE_ADMIN_VERSION + ARG HEADSCALE_ADMIN_ENDPOINT + + # hadolint ignore=DL3018 + RUN apk --no-cache upgrade; \ + apk add --no-cache --virtual BuildTimeDeps git; + + WORKDIR /app + ENV ENDPOINT="${HEADSCALE_ADMIN_ENDPOINT}" + RUN git clone --depth 1 --branch "${HEADSCALE_ADMIN_VERSION}" "${HEADSCALE_ADMIN_REPO}" . && \ + npm install && \ + npm run build && \ + mv /app/build "/app${HEADSCALE_ADMIN_ENDPOINT}" && \ + apk del BuildTimeDeps + +# Build our main image +FROM alpine:${MAIN_IMAGE_ALPINE_VERSION} + # Set SHELL flags for RUN commands to allow -e and pipefail + # Rationale: https://github.com/hadolint/hadolint/wiki/DL4006 + SHELL ["/bin/ash", "-eo", "pipefail", "-c"] + + # Import our "global" `ARG` values into this stage + ARG HEADSCALE_DOWNLOAD_URL + ARG HEADSCALE_SHA256 + ARG LITESTREAM_DOWNLOAD_URL + ARG LITESTREAM_SHA256 + + # Upgrade system and install various dependencies + # - BusyBox's wget isn't reliable enough + # - I'm gonna need a better shell + # - gettext provides `envsubst` for templating + # hadolint ignore=DL3018,SC2086 + RUN BUILD_DEPS="wget"; \ + RUNTIME_DEPS="bash gettext"; \ + apk --no-cache upgrade; \ + apk add --no-cache --virtual BuildTimeDeps ${BUILD_DEPS}; \ + apk add --no-cache ${RUNTIME_DEPS} + + # Copy caddy from the first stage + COPY --from=caddy-builder /usr/bin/caddy /usr/local/bin/caddy + # Caddy smoke test + RUN [ "$(command -v caddy)" = '/usr/local/bin/caddy' ]; \ + caddy version + + # Headscale + RUN set -ex; { \ + wget --retry-connrefused \ + --waitretry=1 \ + --read-timeout=20 \ + --timeout=15 \ + -t 0 \ + -q \ + -O headscale \ + ${HEADSCALE_DOWNLOAD_URL} || { \ + echo "Failed to download Headscale from ${HEADSCALE_DOWNLOAD_URL}"; \ + exit 1; \ + }; \ + echo "${HEADSCALE_SHA256} *headscale" | sha256sum -c - >/dev/null 2>&1; \ + chmod +x headscale; \ + mv headscale /usr/local/bin/; \ + }; \ + # Headscale smoke test + [ "$(command -v headscale)" = '/usr/local/bin/headscale' ]; \ + headscale version; + + # Litestream + RUN set -ex; { \ + wget --retry-connrefused \ + --waitretry=1 \ + --read-timeout=20 \ + --timeout=15 \ + -t 0 \ + -q \ + -O litestream.tar.gz \ + ${LITESTREAM_DOWNLOAD_URL} \ + ; \ + echo "${LITESTREAM_SHA256} *litestream.tar.gz" | sha256sum -c - >/dev/null 2>&1; \ + tar -xf litestream.tar.gz; \ + mv litestream /usr/local/bin/; \ + rm -f litestream.tar.gz; \ + }; \ + # Litestream smoke test + [ "$(command -v litestream)" = '/usr/local/bin/litestream' ]; \ + litestream version; + + # Headscale web GUI + COPY --from=admin-gui /app/admin/ /admin-gui/admin/ + + # Remove build-time dependencies + RUN apk del BuildTimeDeps + + # Copy configuration templates + COPY ./templates/headscale.template.yaml /etc/headscale/config.yaml + COPY ./templates/litestream.template.yml /etc/litestream.yml + COPY ./templates/Caddyfile-http.template /etc/caddy/Caddyfile-http + COPY ./templates/Caddyfile-https.template /etc/caddy/Caddyfile-https + + # Copy and setup scripts into a safe bin directory + COPY --chmod=755 ./scripts/ /usr/local/bin/ + + # Default HTTPS port - override with $PUBLIC_LISTEN_PORT environment variable + EXPOSE 443 + + # Health check to ensure services are running + HEALTHCHECK --interval=30s --timeout=10s --start-period=30s --retries=3 \ + CMD headscale version && caddy version || exit 1 + + ENTRYPOINT ["/usr/local/bin/container-entrypoint.sh"] From cf90502092764e273b3dcda7fea7ef2dd8535d6f Mon Sep 17 00:00:00 2001 From: Ed Geraghty Date: Thu, 2 Jul 2026 14:44:15 +0100 Subject: [PATCH 15/16] Ensure we don't drift back to CRLF --- .gitattributes | 1 + 1 file changed, 1 insertion(+) create mode 100644 .gitattributes diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..6313b56 --- /dev/null +++ b/.gitattributes @@ -0,0 +1 @@ +* text=auto eol=lf From 991f41bcc338da46fdeff25d341965e0ba7c1253 Mon Sep 17 00:00:00 2001 From: Ed Geraghty Date: Thu, 2 Jul 2026 14:52:51 +0100 Subject: [PATCH 16/16] Fail config drift outright rather than running again Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- .github/workflows/headscale-config-checker.yml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.github/workflows/headscale-config-checker.yml b/.github/workflows/headscale-config-checker.yml index 8d12bbd..1ed5b56 100644 --- a/.github/workflows/headscale-config-checker.yml +++ b/.github/workflows/headscale-config-checker.yml @@ -44,4 +44,6 @@ jobs: - name: Fail when drift check fails if: steps.check.outcome == 'failure' - run: make check-config-drift + run: | + echo 'Headscale config drift check failed (see logs above).' + exit 1