From 6a21d9ba12a6c5dab3c17b93f7c60fd49c270653 Mon Sep 17 00:00:00 2001 From: Bryan Date: Wed, 23 Sep 2026 20:10:26 +0000 Subject: [PATCH] ci(bind): require current package description --- .github/ci/check-bind-pkg-descr.sh | 43 ++++++ .../ci/ci-tests/test_bind_pkg_descr_guard.py | 128 ++++++++++++++++++ .../test_bind_pull_request_workflow.py | 12 ++ .github/workflows/bind-tests.yml | 20 +++ dns/bind/pkg-descr | 16 +++ docs/building.md | 4 + 6 files changed, 223 insertions(+) create mode 100755 .github/ci/check-bind-pkg-descr.sh create mode 100644 .github/ci/ci-tests/test_bind_pkg_descr_guard.py diff --git a/.github/ci/check-bind-pkg-descr.sh b/.github/ci/check-bind-pkg-descr.sh new file mode 100755 index 0000000000..2ed331d58c --- /dev/null +++ b/.github/ci/check-bind-pkg-descr.sh @@ -0,0 +1,43 @@ +#!/bin/sh + +set -eu + +if [ "$#" -ne 2 ]; then + echo "usage: $0 " >&2 + exit 2 +fi + +base=$1 +head=$2 +base=$(git merge-base "$base" "$head") +description_changed=false +runtime_changed=false +changed_paths=$(git diff --no-renames --name-only "$base" "$head" -- dns/bind) + +while IFS= read -r path; do + case "$path" in + dns/bind/pkg-descr) + description_changed=true + ;; + dns/bind/src/*|dns/bind/+*) + runtime_changed=true + ;; + dns/bind/Makefile) + if git diff --unified=0 "$base" "$head" -- "$path" | + awk ' + /^[+-]PLUGIN_[A-Z0-9_]+[[:space:]]*[?+:!]?=/ && + $0 !~ /^[+-]PLUGIN_REVISION[[:space:]]*=/ { changed = 1 } + END { exit !changed } + '; then + runtime_changed=true + fi + ;; + esac +done <&2 + exit 1 +fi diff --git a/.github/ci/ci-tests/test_bind_pkg_descr_guard.py b/.github/ci/ci-tests/test_bind_pkg_descr_guard.py new file mode 100644 index 0000000000..a7dacf4787 --- /dev/null +++ b/.github/ci/ci-tests/test_bind_pkg_descr_guard.py @@ -0,0 +1,128 @@ +import pathlib +import subprocess + +import pytest + + +REPOSITORY_ROOT = pathlib.Path(__file__).resolve().parents[3] +GUARD = REPOSITORY_ROOT / ".github/ci/check-bind-pkg-descr.sh" + + +def git(repository: pathlib.Path, *arguments: str) -> str: + return subprocess.run( + ["git", "-C", repository, *arguments], + check=True, + capture_output=True, + text=True, + ).stdout.strip() + + +def initialize_repository(repository: pathlib.Path) -> str: + repository.mkdir() + git(repository, "init") + git(repository, "config", "user.email", "tests@example.invalid") + git(repository, "config", "user.name", "Description guard tests") + initial = { + "dns/bind/Makefile": "PLUGIN_VERSION= 1.0\nPLUGIN_REVISION= 1\nPLUGIN_DEPENDS= bind920\n", + "dns/bind/pkg-descr": "Initial description\n", + "dns/bind/src/service": "initial\n", + "dns/bind/tests/test_service.py": "initial\n", + } + for name, contents in initial.items(): + destination = repository / name + destination.parent.mkdir(parents=True, exist_ok=True) + destination.write_text(contents, encoding="utf-8") + git(repository, "add", ".") + git(repository, "commit", "-m", "initial") + return git(repository, "rev-parse", "HEAD") + + +def run_guard(repository: pathlib.Path, base: str, head: str) -> int: + return subprocess.run( + [str(GUARD), base, head], + cwd=repository, + capture_output=True, + text=True, + ).returncode + + +def check_case(repository: pathlib.Path, changes: dict[str, str]) -> int: + base = initialize_repository(repository) + for name, contents in changes.items(): + destination = repository / name + destination.parent.mkdir(parents=True, exist_ok=True) + destination.write_text(contents, encoding="utf-8") + git(repository, "add", ".") + git(repository, "commit", "-m", "change") + return run_guard(repository, base, "HEAD") + + +@pytest.mark.parametrize( + ("case", "changes", "expected"), + [ + ("runtime", {"dns/bind/src/service": "changed\n"}, 1), + ("hook", {"dns/bind/+POST_INSTALL.post": "changed\n"}, 1), + ( + "documented", + { + "dns/bind/src/service": "changed\n", + "dns/bind/pkg-descr": "Updated description\n", + }, + 0, + ), + ( + "dependency-formula", + { + "dns/bind/Makefile": ( + "PLUGIN_VERSION= 1.0\nPLUGIN_REVISION= 1\nPLUGIN_DEPENDS= bind920\n" + "PLUGIN_DEPEND_FORMULA_DEPENDS= bind920\n" + ) + }, + 1, + ), + ( + "manifest-dependency", + { + "dns/bind/Makefile": ( + "PLUGIN_VERSION= 1.0\nPLUGIN_REVISION= 1\nPLUGIN_DEPENDS= bind920\n" + "PLUGIN_MANIFEST_DEPENDS= bind920\n" + ) + }, + 1, + ), + ( + "revision", + {"dns/bind/Makefile": "PLUGIN_VERSION= 1.0\nPLUGIN_REVISION= 2\nPLUGIN_DEPENDS= bind920\n"}, + 0, + ), + ("tests", {"dns/bind/tests/test_service.py": "changed\n"}, 0), + ], +) +def test_guard_requires_pkg_descr_only_for_publishable_changes(tmp_path, case, changes, expected): + assert check_case(tmp_path / case, changes) == expected + + +def test_guard_uses_merge_base_when_the_release_branch_advances(tmp_path): + repository = tmp_path / "advanced-base" + original_base = initialize_repository(repository) + git(repository, "checkout", "-b", "feature") + (repository / "dns/bind/src/service").write_text("changed\n", encoding="utf-8") + git(repository, "commit", "-am", "runtime change") + head = git(repository, "rev-parse", "HEAD") + + git(repository, "checkout", original_base) + (repository / "dns/bind/pkg-descr").write_text("Base advanced\n", encoding="utf-8") + git(repository, "commit", "-am", "advance base") + advanced_base = git(repository, "rev-parse", "HEAD") + + assert original_base == git(repository, "merge-base", advanced_base, head) + assert run_guard(repository, advanced_base, head) == 1 + + +def test_guard_detects_runtime_files_moved_out_of_the_package(tmp_path): + repository = tmp_path / "runtime-rename" + base = initialize_repository(repository) + git(repository, "mv", "dns/bind/src/service", "dns/bind/tests/moved_service") + git(repository, "commit", "-m", "move runtime file") + + assert run_guard(repository, base, "HEAD") == 1 diff --git a/.github/ci/ci-tests/test_bind_pull_request_workflow.py b/.github/ci/ci-tests/test_bind_pull_request_workflow.py index 4591a14666..5482279a26 100644 --- a/.github/ci/ci-tests/test_bind_pull_request_workflow.py +++ b/.github/ci/ci-tests/test_bind_pull_request_workflow.py @@ -74,6 +74,18 @@ def test_release_source_pull_requests_materialize_master_ci_helpers(): assert '.resolver-plugins/bind920.json' in helper_job +def test_workflow_requires_pkg_descr_for_publishable_bind_changes(): + workflow = workflow_text() + changes_job = workflow.split(' changes:', 1)[1].split(' ci-helpers:', 1)[0] + + assert 'Check BIND package description freshness' in changes_job + assert 'CALLER_SHA: ${{ inputs.pull_request_sha }}' in changes_job + assert 'if [ -n "$CALLER_SHA" ]; then' in changes_job + assert 'refs/heads/$PR_BASE:refs/remotes/origin/pr-base' in changes_job + assert 'refs/heads/master:refs/remotes/origin/control-plane' in changes_job + assert 'check-bind-pkg-descr.sh' in changes_job + + def test_reusable_workflow_accepts_the_callers_pull_request_context(): workflow = workflow_text() diff --git a/.github/workflows/bind-tests.yml b/.github/workflows/bind-tests.yml index 5bb8b06442..994e365743 100644 --- a/.github/workflows/bind-tests.yml +++ b/.github/workflows/bind-tests.yml @@ -60,6 +60,26 @@ jobs: esac done < "$RUNNER_TEMP/changed-paths" printf 'bind_source=%s\nci_helpers=%s\n' "$bind_source" "$ci_helpers" >> "$GITHUB_OUTPUT" + - name: Check BIND package description freshness + shell: bash + env: + CALLER_SHA: ${{ inputs.pull_request_sha }} + PR_BASE: ${{ inputs.pull_request_base || github.event.pull_request.base.ref }} + BASE_SHA: ${{ github.event.pull_request.base.sha }} + HEAD_SHA: ${{ inputs.pull_request_sha || github.event.pull_request.head.sha }} + run: | + set -euo pipefail + guard=.github/ci/check-bind-pkg-descr.sh + if [ -n "$CALLER_SHA" ]; then + git fetch --no-tags origin \ + "refs/heads/$PR_BASE:refs/remotes/origin/pr-base" \ + 'refs/heads/master:refs/remotes/origin/control-plane' + BASE_SHA=$(git rev-parse refs/remotes/origin/pr-base) + guard="$RUNNER_TEMP/check-bind-pkg-descr.sh" + git show refs/remotes/origin/control-plane:.github/ci/check-bind-pkg-descr.sh > "$guard" + chmod +x "$guard" + fi + "$guard" "$BASE_SHA" "$HEAD_SHA" ci-helpers: needs: changes diff --git a/dns/bind/pkg-descr b/dns/bind/pkg-descr index 284434dc92..9984795802 100644 --- a/dns/bind/pkg-descr +++ b/dns/bind/pkg-descr @@ -7,6 +7,22 @@ necessary for asking and answering name service questions. Plugin Changelog ================ +1.36 + +* Bound BIND shutdown and log forced recovery steps in the General Log +* Preserve dynamic BIND records while package upgrades regenerate zone files +* Reconcile the supported BIND package pair after plugin and system upgrades +* Add DHCP lease watcher and scoped dynamic DNS mappings +* Add reverse DNS zone management +* Source DNSBL lists from the Unbound definitions +* Add Forward First / Forward Only mode + +1.35 + +* Add per-forwarder destination port for plain DNS forwarders +* Add DNS-over-TLS (DoT) forwarders with TLS hostname verification +* Migrate legacy DNS Forwarders list to the new DNS Forwarders tab + 1.34 * Add custom configuration include directory /usr/local/etc/namedb/named.conf.d (contributed by Nicholas Card) diff --git a/docs/building.md b/docs/building.md index fd322fc35a..a914333f1c 100644 --- a/docs/building.md +++ b/docs/building.md @@ -177,6 +177,10 @@ the CI helper tests for BIND profile changes, including generated `.resolver-plugins/bind920.json` candidate PRs. Do not add a static release matrix: a newly created release branch is included automatically. +The workflow also requires `dns/bind/pkg-descr` to change with publishable +BIND runtime, package-hook, or package-metadata changes. Test-only changes and +`PLUGIN_REVISION`-only rebuilds do not require a changelog entry. + ## BIND candidate updates The `Propose bind920 candidate` workflow is manual-only. It inspects a