From 5251b225b905f72b12095c03d5460692aa5f0fd6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marcos=20M=C3=A9ndez?= Date: Mon, 28 Sep 2026 02:55:05 +0000 Subject: [PATCH 1/4] feat: write /etc/keel_version beside the compatibility file An appliance had no file saying what it is. The console banner reads /etc/turnkey_version for the appliance name and the version because there was nothing else to read, and its own comment said so; everything else Keel presents to an operator would have had to do the same. /etc/keel_version is that file: the same four fields, keel----, written at build time beside /etc/turnkey_version. Decision 0014 settles what each of the two is for: the TurnKey file is an interface we honour, the Keel file is what we say we are. Why here and not in an appliance conf script. A conf script runs in root.patched/body, and /etc/turnkey_version is written in root.patched/post, so at the moment a conf script runs the file it would derive the Keel name from still holds the parent layer's value, or nothing at all on a rootfs layer. The version string exists in exactly one place, the recipe that computes it from the product changelog, and that place is shared by every appliance rather than repeated in each of them. Measured from the makefiles themselves, root.patched/post is also the last thing that touches the tree before the removelists, so neither file can be clobbered by an overlay or a conf script. The prefix is normalised, which fixes a defect this work uncovered. turnkey-version.py takes the name from the first line of the product changelog, so a repository that renames its release package produces a version string with that name's prefix. keel-core's changelog became "keel-core-19.0 (1) keel" on 2026-09-27, so the next core layer would have written keel-core-19.0-trixie-amd64 into /etc/turnkey_version, and every parser of that file is prefix sensitive: - sysversion._parse_turnkey_release matches "turnkey-.*?-(\d.*?)-[^\d]", so get_turnkey_release() returns the empty string and the release number disappears from everything that formats a version; - sysversion.AppVer removes the prefix "turnkey-" and then splits, so appname becomes "keel-core" instead of "core"; - keel.inspect.app.probe_appliance requires the string to start with "turnkey-" and reports the appliance as missing otherwise, which costs keel inspect and keel diff the appliance identity of the machine. bin/keel-version-files now takes the changelog-derived string, removes at most one product prefix and writes turnkey- and keel-. The published core layer predates the changelog rename, so nothing shipped is affected; the next build would have been. The grammar and the prefix rules are lib/version-files.sh, pure functions with no effect, and the script is the thin main that validates and writes (decision 0004). Both are measured: 100 percent, 15 of 15 and 36 of 36 lines, 33 bats tests under kcov 43, covering every exit code (1 bad usage or unparseable version, 2 an unwritable tree, 3 the library missing) and the errexit trap of docs/traps.md. tests/coverage.sh grew the per-target loop keel-core uses so it can measure more than one file; conf/turnkey.d/postfix-local stays at 100 percent, 17 of 17 lines. --- COVERAGE.md | 24 +++- bin/keel-version-files | 87 ++++++++++++++ lib/version-files.sh | 71 +++++++++++ mk/turnkey.mk | 12 +- tests/coverage.sh | 2 + tests/version-files.bats | 251 +++++++++++++++++++++++++++++++++++++++ 6 files changed, 442 insertions(+), 5 deletions(-) create mode 100755 bin/keel-version-files create mode 100644 lib/version-files.sh create mode 100644 tests/version-files.bats diff --git a/COVERAGE.md b/COVERAGE.md index 714ce819..9798c425 100644 --- a/COVERAGE.md +++ b/COVERAGE.md @@ -148,8 +148,10 @@ Pull request #2 merged on 2026-09-26 (merge commit 5a0a381) and brought | `conf/turnkey.d/postfix-local` | the build-time postfix configuration | 100 percent, 17 of 17 lines, 7 bats | | `conf/turnkey.d/dpkg-vendor` | points the dpkg vendor at Keel, and removes an inherited TurnKey origin | 100 percent, 7 of 7 lines, 16 bats | | `conf/turnkey.d/apt-identity` | keeps the shipped apt User-Agent the one in force | 100 percent, 4 of 4 lines, 11 bats | +| `lib/version-files.sh` | the grammar and the prefix rules of the two identity files (decision 0014) | 100 percent, 15 of 15 lines | +| `bin/keel-version-files` | the thin main `mk/turnkey.mk` calls in `root.patched/post` to write `/etc/turnkey_version` and `/etc/keel_version` | 100 percent, 36 of 36 lines | -34 bats, measured on 2026-09-29 with kcov 43 and bats 1.11. The gate in +34 bats, measured on 2026-09-29 with kcov 43 and bats 1.11, plus the 33 bats of the identity files (`tests/version-files.bats`, `tests/mk-identity.bats`), measured on 2026-09-28. The gate in `.github/workflows/tests.yml` is set to 100, the measured number, and is only ever raised. The sections that follow record the state before the first merge. @@ -193,6 +195,26 @@ they were last. All three are `run !` now, so none of them depends on its position, and the check runs for every repository in the reusable `test-shell` workflow. +## The two identity files + +`mk/turnkey.mk` writes both in `root.patched/post`, after every overlay, +conf script, patch and removelist of the build, so nothing can clobber +them. The version string comes from the first line of the product +changelog through fab's `turnkey-version.py`, and its prefix is normalised +before either file is written: + +- `/etc/turnkey_version` always begins `turnkey-`, because it is an + interface: `sysversion`, the `turnkey-version` command, inithooks' + `29tagid` and `keel inspect` all parse it by prefix, and `keel inspect` + drops the appliance identity outright for a string that begins with + anything else. +- `/etc/keel_version` is the same four fields with the `keel-` prefix, and + is what the appliance reads when it says what it is. + +Without the normalisation a repository that renames its release package +(`keel-core-19.0`, as keel-core did on 2026-09-27) silently produces an +`/etc/turnkey_version` that none of those parsers accepts. + ## Baseline before the merge: 0 percent, nothing measured This repository has no test suite and no coverage tool wired up, so nothing diff --git a/bin/keel-version-files b/bin/keel-version-files new file mode 100755 index 00000000..de88e257 --- /dev/null +++ b/bin/keel-version-files @@ -0,0 +1,87 @@ +#!/bin/bash +# Writes the two identity files of an image being built: the compatibility +# file /etc/turnkey_version and the Keel file /etc/keel_version (decision +# 0014). Called once per product by mk/turnkey.mk, in root.patched/post, +# with the version string fab's turnkey-version.py derived from the first +# line of the product changelog and the root of the tree being built. +# +# This is the thin main of lib/version-files.sh (decision 0004): it parses +# the arguments, refuses what it cannot name and writes the two files; the +# grammar and the prefix rules are in the library and are unit tested. +# +# exit 0 both files written +# exit 1 bad usage, or a version string that is not an appliance identity +# exit 2 the tree cannot be written to +# exit 3 the library is missing +set -euo pipefail + +here=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd) +lib=${KVF_LIB:-$here/../lib/version-files.sh} + +usage() { + cat <&2 + echo "keel-version-files: two arguments are required, got $#" >&2 + exit 1 +fi + +if [ ! -r "$lib" ]; then + echo "keel-version-files: $lib is missing; it is lib/version-files.sh of common" >&2 + exit 3 +fi +# shellcheck source=../lib/version-files.sh +. "$lib" + +release=$1 +root=$2 + +app_version=$(kvf_app_version "$release") +if ! kvf_is_app_version "$app_version"; then + echo "keel-version-files: '$release' is not an appliance identity; the" \ + "first line of the changelog must name a release whose version" \ + "string is app-version-codename-architecture, as in" \ + "turnkey-core-19.0 or keel-core-19.0" >&2 + exit 1 +fi + +if [ ! -d "$root" ]; then + echo "keel-version-files: '$root' is not a directory" >&2 + exit 2 +fi + +etc=$root/etc +mkdir -p "$etc" 2>/dev/null || true +if [ ! -d "$etc" ] || [ ! -w "$etc" ]; then + echo "keel-version-files: cannot write into '$etc'" >&2 + exit 2 +fi + +write_identity() { + # write_identity PREFIX FILE + local string + string=$(kvf_version_string "$1" "$app_version") + printf '%s\n' "$string" > "$etc/$2" + chmod 0644 "$etc/$2" + echo "$etc/$2: $string" +} + +write_identity "$KVF_TURNKEY_PREFIX" turnkey_version +write_identity "$KVF_KEEL_PREFIX" keel_version diff --git a/lib/version-files.sh b/lib/version-files.sh new file mode 100644 index 00000000..43ff7192 --- /dev/null +++ b/lib/version-files.sh @@ -0,0 +1,71 @@ +#!/bin/bash +# Pure helpers of bin/keel-version-files (decision 0004: logic apart from +# effect). Nothing here writes a file, runs a command, reads the clock or +# looks at anything but its arguments. Sourced by bin/keel-version-files +# and by tests/version-files.bats. +# +# What the two identity files are, and why there are two (decision 0014): +# +# /etc/turnkey_version the compatibility contract. Its name and its +# grammar, turnkey----, +# are an interface: the sysversion library, the +# turnkey-version command, inithooks' 29tagid and +# keel's own inspect all parse it, and every one of +# those parsers is prefix sensitive. It therefore +# always begins with "turnkey-", whatever the +# product's changelog calls the release package. +# /etc/keel_version what this appliance says it is. Same grammar, +# "keel-" prefix, and the file everything Keel +# presents to an operator reads first. +# +# The input is the string fab's turnkey-version.py builds from the first +# line of the product changelog, which is why the prefix cannot be trusted: +# a repository that renames its release package (keel-core-19.0, as +# keel-core did on 2026-09-27) would otherwise write an /etc/turnkey_version +# that none of those parsers accepts. + +# Which prefixes name a product rather than an appliance. +# shellcheck disable=SC2034 # KVF_TURNKEY_PREFIX and _KEEL_PREFIX are read by the caller +KVF_PREFIXES="turnkey keel" +KVF_TURNKEY_PREFIX=turnkey +KVF_KEEL_PREFIX=keel + +# kvf_app_version RELEASE_NAME +# The --- part of a release version string, +# with at most one product prefix removed: turnkey-wordpress-19.0-trixie-amd64 +# and keel-core-19.0-trixie-amd64 both lose their first field, a name with +# neither prefix is returned whole, and an app whose own name starts with +# the other product's name keeps it. +kvf_app_version() { + local text=${1-} prefix + for prefix in $KVF_PREFIXES; do + if [ "${text#"$prefix"-}" != "$text" ]; then + printf '%s\n' "${text#"$prefix"-}" + return 0 + fi + done + printf '%s\n' "$text" +} + +# kvf_is_app_version TEXT +# TEXT carries the four fields an appliance identity needs: +# ---, app lower case and possibly +# hyphenated, version starting with a digit so a release tag such as +# 19.0rc is part of it, codename and architecture one field each. A string +# that still carries a product prefix fails, because "turnkey" would then +# be read as the app: the prefix comes off first, with kvf_app_version. +kvf_is_app_version() { + local text=${1-} prefix + for prefix in $KVF_PREFIXES; do + if [ "${text#"$prefix"-}" != "$text" ]; then + return 1 + fi + done + [[ $text =~ ^[a-z0-9][a-z0-9.+-]*-[0-9][^-]*-[a-z][a-z0-9]*-[a-z0-9]+$ ]] +} + +# kvf_version_string PREFIX APP_VERSION +# One identity string: the product prefix and the four fields. +kvf_version_string() { + printf '%s-%s\n' "${1-}" "${2-}" +} diff --git a/mk/turnkey.mk b/mk/turnkey.mk index 2b475863..4b581fa1 100644 --- a/mk/turnkey.mk +++ b/mk/turnkey.mk @@ -27,6 +27,9 @@ COMMON_REMOVELISTS += turnkey COMMON_REMOVELISTS_FINAL += turnkey FAB_SHARE_PATH ?= /usr/share/fab +# This repository, as the build sees it. bin/keel-version-files writes the +# two identity files of the image (decision 0014). +COMMON_BIN_PATH ?= $(FAB_PATH)/common/bin APT_OVERLAY = fab-apply-overlay $(COMMON_OVERLAYS_PATH)/bootstrap_apt $O/bootstrap; @@ -51,7 +54,8 @@ define _bootstrap/post endef bootstrap/post += $(_bootstrap/post) -# set /etc/turnkey_version +# set /etc/turnkey_version and /etc/keel_version (bin/keel-version-files, +# decision 0014) # # fab's release meta package (turnkey--) is no longer built: # keel-core is the meta package of a Keel image (handbook decision 0047), @@ -65,11 +69,11 @@ bootstrap/post += $(_bootstrap/post) define _root.patched/post # - # setting /etc/turnkey_version + # setting /etc/turnkey_version and /etc/keel_version # @if [ -f ./changelog ]; then \ - turnkey_version=$$($(FAB_SHARE_PATH)/turnkey-version.py --dist=$(CODENAME) --tag=$(VERSION_TAG) ./changelog $(FAB_ARCH)); \ - echo $$turnkey_version > $O/root.patched/etc/turnkey_version; \ + release_version=$$($(FAB_SHARE_PATH)/turnkey-version.py --dist=$(CODENAME) --tag=$(VERSION_TAG) ./changelog $(FAB_ARCH)); \ + $(COMMON_BIN_PATH)/keel-version-files $$release_version $O/root.patched || exit 1; \ else \ echo; \ echo "WARNING: can't tag local release (./changelog doesn't exist)"; \ diff --git a/tests/coverage.sh b/tests/coverage.sh index 5062e1b1..bbb53498 100755 --- a/tests/coverage.sh +++ b/tests/coverage.sh @@ -37,6 +37,8 @@ targets=( "overlays/turnkey.d/resolvconf-ifupdown-ng/etc/network/if-down.d/resolvconf-ifupdown-ng:tests/resolvconf-ifupdown-ng.bats" "packages/coraza/state:tests/coraza-state.bats" "packages/anubis/signing-key:tests/anubis-signing-key.bats" + "lib/version-files.sh:tests/version-files.bats" + "bin/keel-version-files:tests/version-files.bats" ) for tool in kcov bats; do diff --git a/tests/version-files.bats b/tests/version-files.bats new file mode 100644 index 00000000..8dccbcfd --- /dev/null +++ b/tests/version-files.bats @@ -0,0 +1,251 @@ +#!/usr/bin/env bats +# Unit tests of lib/version-files.sh and bin/keel-version-files: the two +# identity files an image carries, /etc/turnkey_version (the interface we +# honour) and /etc/keel_version (what we say we are), decision 0014. +# +# Nothing here needs root, a chroot or a build: the script is given a +# version string and a directory, and writes two files into it. + +bats_require_minimum_version 1.5.0 + +setup() { + ROOT="$(cd "$BATS_TEST_DIRNAME/.." && pwd)" + LIB="$ROOT/lib/version-files.sh" + SCRIPT="$ROOT/bin/keel-version-files" + load ../lib/version-files.sh + SCRATCH="$BATS_TEST_TMPDIR/scratch" + mkdir -p "$SCRATCH" +} + +# the app part of a release version string + +@test "app_version: the turnkey- prefix of a changelog name is dropped" { + run kvf_app_version turnkey-wordpress-19.0-trixie-amd64 + [ "$status" -eq 0 ] + [ "$output" = wordpress-19.0-trixie-amd64 ] +} + +@test "app_version: the keel- prefix of a changelog name is dropped" { + run kvf_app_version keel-core-19.0-trixie-amd64 + [ "$status" -eq 0 ] + [ "$output" = core-19.0-trixie-amd64 ] +} + +@test "app_version: a name with neither prefix is kept whole" { + run kvf_app_version core-19.0-trixie-amd64 + [ "$status" -eq 0 ] + [ "$output" = core-19.0-trixie-amd64 ] +} + +@test "app_version: only one prefix is dropped, never two" { + run kvf_app_version keel-turnkey-core-19.0-trixie-amd64 + [ "$output" = turnkey-core-19.0-trixie-amd64 ] +} + +@test "app_version: an app name that begins with the other prefix survives" { + run kvf_app_version turnkey-keelson-19.0-trixie-amd64 + [ "$output" = keelson-19.0-trixie-amd64 ] +} + +@test "app_version: an empty string stays empty" { + run kvf_app_version "" + [ "$status" -eq 0 ] + [ -z "$output" ] +} + +@test "app_version: no argument is the same as an empty string" { + run kvf_app_version + [ "$status" -eq 0 ] + [ -z "$output" ] +} + +# the grammar of the four fields + +@test "is_app_version: the four fields of an appliance" { + run kvf_is_app_version core-19.0-trixie-amd64 + [ "$status" -eq 0 ] +} + +@test "is_app_version: an app name with hyphens keeps them" { + run kvf_is_app_version nginx-php-fastcgi-19.0-trixie-amd64 + [ "$status" -eq 0 ] +} + +@test "is_app_version: a release tag is part of the version field" { + run kvf_is_app_version core-19.0rc-trixie-amd64 + [ "$status" -eq 0 ] +} + +@test "is_app_version: another architecture" { + run kvf_is_app_version core-19.0-trixie-arm64 + [ "$status" -eq 0 ] +} + +@test "is_app_version: a string missing the architecture is refused" { + run kvf_is_app_version core-19.0-trixie + [ "$status" -eq 1 ] +} + +@test "is_app_version: a version that does not start with a digit is refused" { + run kvf_is_app_version core-nineteen-trixie-amd64 + [ "$status" -eq 1 ] +} + +@test "is_app_version: upper case is refused" { + run kvf_is_app_version Core-19.0-trixie-amd64 + [ "$status" -eq 1 ] +} + +@test "is_app_version: an empty string is refused" { + run kvf_is_app_version "" + [ "$status" -eq 1 ] +} + +@test "is_app_version: no argument is refused" { + run kvf_is_app_version + [ "$status" -eq 1 ] +} + +@test "is_app_version: a prefixed string is refused, the prefix goes first" { + run kvf_is_app_version turnkey-core-19.0-trixie-amd64 + [ "$status" -eq 1 ] +} + +# the two strings + +@test "version_string: the compatibility string and the Keel string" { + run kvf_version_string turnkey core-19.0-trixie-amd64 + [ "$output" = turnkey-core-19.0-trixie-amd64 ] + run kvf_version_string keel core-19.0-trixie-amd64 + [ "$output" = keel-core-19.0-trixie-amd64 ] +} + +# the script + +@test "script: a turnkey changelog name writes both files" { + run "$SCRIPT" turnkey-wordpress-19.0-trixie-amd64 "$SCRATCH" + [ "$status" -eq 0 ] + [ "$(cat "$SCRATCH/etc/turnkey_version")" = turnkey-wordpress-19.0-trixie-amd64 ] + [ "$(cat "$SCRATCH/etc/keel_version")" = keel-wordpress-19.0-trixie-amd64 ] +} + +@test "script: a keel changelog name still writes the compatibility file" { + # The case that motivated the check: keel-core's changelog was renamed + # to keel-core-19.0, and /etc/turnkey_version is parsed by prefix. + run "$SCRIPT" keel-core-19.0-trixie-amd64 "$SCRATCH" + [ "$status" -eq 0 ] + [ "$(cat "$SCRATCH/etc/turnkey_version")" = turnkey-core-19.0-trixie-amd64 ] + [ "$(cat "$SCRATCH/etc/keel_version")" = keel-core-19.0-trixie-amd64 ] +} + +@test "script: the two files differ in the prefix and nothing else" { + "$SCRIPT" keel-nginx-php-fastcgi-19.0-trixie-amd64 "$SCRATCH" + turnkey=$(cat "$SCRATCH/etc/turnkey_version") + keel=$(cat "$SCRATCH/etc/keel_version") + [ "${turnkey#turnkey-}" = "${keel#keel-}" ] +} + +@test "script: etc is created when the tree does not have it yet" { + [ ! -d "$SCRATCH/etc" ] + run "$SCRIPT" turnkey-core-19.0-trixie-amd64 "$SCRATCH" + [ "$status" -eq 0 ] + [ -d "$SCRATCH/etc" ] +} + +@test "script: each file is one line, world readable" { + "$SCRIPT" turnkey-core-19.0-trixie-amd64 "$SCRATCH" + [ "$(wc -l < "$SCRATCH/etc/turnkey_version")" -eq 1 ] + [ "$(wc -l < "$SCRATCH/etc/keel_version")" -eq 1 ] + [ "$(stat -c %a "$SCRATCH/etc/turnkey_version")" = 644 ] + [ "$(stat -c %a "$SCRATCH/etc/keel_version")" = 644 ] +} + +@test "script: an existing pair is replaced, not appended to" { + mkdir -p "$SCRATCH/etc" + printf 'turnkey-core-18.0-bookworm-amd64\n' > "$SCRATCH/etc/turnkey_version" + printf 'keel-core-18.0-bookworm-amd64\n' > "$SCRATCH/etc/keel_version" + "$SCRIPT" turnkey-core-19.0-trixie-amd64 "$SCRATCH" + [ "$(cat "$SCRATCH/etc/turnkey_version")" = turnkey-core-19.0-trixie-amd64 ] + [ "$(wc -l < "$SCRATCH/etc/turnkey_version")" -eq 1 ] +} + +@test "script: a version string it cannot name is refused and nothing is written" { + run "$SCRIPT" not-a-version "$SCRATCH" + [ "$status" -eq 1 ] + [[ $output == *"not-a-version"* ]] + [[ $output == *"app-version-codename-architecture"* ]] + [ ! -e "$SCRATCH/etc/turnkey_version" ] + [ ! -e "$SCRATCH/etc/keel_version" ] +} + +@test "script: an empty version string is refused" { + run "$SCRIPT" "" "$SCRATCH" + [ "$status" -eq 1 ] +} + +@test "script: too few arguments print the usage and fail" { + run "$SCRIPT" turnkey-core-19.0-trixie-amd64 + [ "$status" -eq 1 ] + [[ $output == *"usage: keel-version-files"* ]] +} + +@test "script: too many arguments print the usage and fail" { + run "$SCRIPT" turnkey-core-19.0-trixie-amd64 "$SCRATCH" extra + [ "$status" -eq 1 ] + [[ $output == *"usage: keel-version-files"* ]] +} + +@test "script: -h prints the usage and succeeds" { + run "$SCRIPT" -h + [ "$status" -eq 0 ] + [[ $output == *"usage: keel-version-files"* ]] + run "$SCRIPT" --help + [ "$status" -eq 0 ] +} + +@test "script: a root that is not a directory is refused" { + run "$SCRIPT" turnkey-core-19.0-trixie-amd64 "$SCRATCH/absent" + [ "$status" -eq 2 ] + [[ $output == *"$SCRATCH/absent"* ]] + [[ $output == *"not a directory"* ]] +} + +@test "script: a tree it cannot write to is refused" { + mkdir -p "$SCRATCH/etc" + chmod 0500 "$SCRATCH/etc" + run "$SCRIPT" turnkey-core-19.0-trixie-amd64 "$SCRATCH" + chmod 0700 "$SCRATCH/etc" + [ "$status" -eq 2 ] + [[ $output == *"cannot write"* ]] +} + +@test "script: the library it needs is named in the failure when it is gone" { + copy="$BATS_TEST_TMPDIR/keel-version-files" + cp "$SCRIPT" "$copy" + run "$copy" turnkey-core-19.0-trixie-amd64 "$SCRATCH" + [ "$status" -eq 3 ] + [[ $output == *"version-files.sh"* ]] +} + +# the library under the caller's own shell options (docs/traps.md, "A bats +# suite cannot see a library that kills its caller"): bats turns errexit +# off, the script that sources this library does not. + +@test "library: sourced under set -euo pipefail, a refusal does not kill the caller" { + cat > "$BATS_TEST_TMPDIR/caller" < Date: Tue, 29 Sep 2026 03:22:03 +0000 Subject: [PATCH 2/4] fix: write the identity files on desktop builds too, and fail legibly mk/turnkey-desktop.mk is a second copy of the root.patched/post block and still wrote /etc/turnkey_version from the raw changelog string, so a desktop build got no /etc/keel_version and none of the prefix normalisation. It now calls bin/keel-version-files like mk/turnkey.mk. Both makefiles check the script is there before calling it. A build host whose common checkout predates it would otherwise die in root.patched/post, after the whole root was built, with a bare "No such file or directory"; now it names the path and says the checkout is stale. And the version string is quoted, so an empty one or one with a space is refused as a version rather than, by luck, as a wrong argument count. tests/mk-identity.bats makes the recipe of both makefiles against stubs of fab and reads the files back; six of its eight tests failed before this change. make has no line coverage, so tests/coverage.sh runs it for its verdict. --- COVERAGE.md | 19 ++++++-- mk/turnkey-desktop.mk | 13 ++++-- mk/turnkey.mk | 3 +- tests/coverage.sh | 12 +++++ tests/mk-identity.bats | 89 ++++++++++++++++++++++++++++++++++++ tests/mk/fab-chroot | 3 ++ tests/mk/harness.mk | 8 ++++ tests/mk/make-release-deb.py | 3 ++ tests/mk/product.mk | 0 tests/mk/turnkey-version.py | 4 ++ 10 files changed, 145 insertions(+), 9 deletions(-) create mode 100644 tests/mk-identity.bats create mode 100755 tests/mk/fab-chroot create mode 100644 tests/mk/harness.mk create mode 100755 tests/mk/make-release-deb.py create mode 100644 tests/mk/product.mk create mode 100755 tests/mk/turnkey-version.py diff --git a/COVERAGE.md b/COVERAGE.md index 9798c425..e78d02e4 100644 --- a/COVERAGE.md +++ b/COVERAGE.md @@ -148,10 +148,10 @@ Pull request #2 merged on 2026-09-26 (merge commit 5a0a381) and brought | `conf/turnkey.d/postfix-local` | the build-time postfix configuration | 100 percent, 17 of 17 lines, 7 bats | | `conf/turnkey.d/dpkg-vendor` | points the dpkg vendor at Keel, and removes an inherited TurnKey origin | 100 percent, 7 of 7 lines, 16 bats | | `conf/turnkey.d/apt-identity` | keeps the shipped apt User-Agent the one in force | 100 percent, 4 of 4 lines, 11 bats | -| `lib/version-files.sh` | the grammar and the prefix rules of the two identity files (decision 0014) | 100 percent, 15 of 15 lines | -| `bin/keel-version-files` | the thin main `mk/turnkey.mk` calls in `root.patched/post` to write `/etc/turnkey_version` and `/etc/keel_version` | 100 percent, 36 of 36 lines | +| `lib/version-files.sh` | the grammar and the prefix rules of the two identity files (decision 0014) | 100 percent, 17 of 17 lines | +| `bin/keel-version-files` | the thin main `mk/turnkey.mk` and `mk/turnkey-desktop.mk` call in `root.patched/post` to write `/etc/turnkey_version` and `/etc/keel_version` | 100 percent, 36 of 36 lines | -34 bats, measured on 2026-09-29 with kcov 43 and bats 1.11, plus the 33 bats of the identity files (`tests/version-files.bats`, `tests/mk-identity.bats`), measured on 2026-09-28. The gate in +34 bats, measured on 2026-09-29 with kcov 43 and bats 1.11, plus the 37 bats of the identity files (`tests/version-files.bats`, and `tests/mk-identity.bats`, which make runs against stubs of fab), measured on 2026-09-29. The gate in `.github/workflows/tests.yml` is set to 100, the measured number, and is only ever raised. The sections that follow record the state before the first merge. @@ -197,7 +197,7 @@ position, and the check runs for every repository in the reusable ## The two identity files -`mk/turnkey.mk` writes both in `root.patched/post`, after every overlay, +`mk/turnkey.mk` and `mk/turnkey-desktop.mk` write both in `root.patched/post`, after every overlay, conf script, patch and removelist of the build, so nothing can clobber them. The version string comes from the first line of the product changelog through fab's `turnkey-version.py`, and its prefix is normalised @@ -215,6 +215,17 @@ Without the normalisation a repository that renames its release package (`keel-core-19.0`, as keel-core did on 2026-09-27) silently produces an `/etc/turnkey_version` that none of those parsers accepts. +The make recipe itself is run, not read: `tests/mk-identity.bats` makes the +`root.patched/post` step of both `mk/turnkey.mk` and `mk/turnkey-desktop.mk` +against stubs of fab under `tests/mk/` and reads the two files back, 8 +tests. It also holds the two ways the call can fail: a `common` checkout +that predates `bin/keel-version-files` stops the build with a message that +names the path and says the checkout is stale, rather than a bare `No such +file or directory` after the whole root was built; and the version string is +quoted, so an empty one or one with a space is refused as a version rather +than as a wrong argument count. make has no line coverage, so +`tests/coverage.sh` runs this suite for its verdict alone. + ## Baseline before the merge: 0 percent, nothing measured This repository has no test suite and no coverage tool wired up, so nothing diff --git a/mk/turnkey-desktop.mk b/mk/turnkey-desktop.mk index 2de6b7a9..b61b9ece 100644 --- a/mk/turnkey-desktop.mk +++ b/mk/turnkey-desktop.mk @@ -30,6 +30,9 @@ COMMON_REMOVELISTS += turnkey COMMON_REMOVELISTS_FINAL += turnkey FAB_SHARE_PATH ?= /usr/share/fab +# This repository, as the build sees it. bin/keel-version-files writes the +# two identity files of the image (decision 0014). +COMMON_BIN_PATH ?= $(FAB_PATH)/common/bin # below hacks allow inheritors to define their own hooks, which will be # prepended. warning: first line *needs* to be empty for this to work @@ -47,7 +50,8 @@ endef bootstrap/post += $(_bootstrap/post) # tag package management system with release package -# set /etc/turnkey_version +# set /etc/turnkey_version and /etc/keel_version (bin/keel-version-files, +# decision 0014) # # The apt User-Agent is no longer written here, for the reason given in # mk/turnkey.mk: overlays/turnkey.d/apt-identity ships it (Keel-Linux/common#6). @@ -55,7 +59,7 @@ define _root.patched/post # # tagging package management system with release package - # setting /etc/turnkey_version + # setting /etc/turnkey_version and /etc/keel_version # @if [ -f $(FAB_PATH)/products/core/changelog ]; then \ echo $(FAB_SHARE_PATH)/make-release-deb.py $(FAB_PATH)/products/core/changelog $O/root.patched; \ @@ -64,8 +68,9 @@ define _root.patched/post @if [ -f ./changelog ]; then \ echo $(FAB_SHARE_PATH)/make-release-deb.py ./changelog $O/root.patched; \ $(FAB_SHARE_PATH)/make-release-deb.py ./changelog $O/root.patched; \ - turnkey_version=$$($(FAB_SHARE_PATH)/turnkey-version.py --dist=$(CODENAME) --tag=$(VERSION_TAG) ./changelog $(FAB_ARCH)); \ - echo $$turnkey_version > $O/root.patched/etc/turnkey_version; \ + release_version=$$($(FAB_SHARE_PATH)/turnkey-version.py --dist=$(CODENAME) --tag=$(VERSION_TAG) ./changelog $(FAB_ARCH)); \ + [ -x $(COMMON_BIN_PATH)/keel-version-files ] || { echo "ERROR: $(COMMON_BIN_PATH)/keel-version-files is missing or not executable: the common checkout predates the identity files of decision 0014, update it" >&2; exit 1; }; \ + $(COMMON_BIN_PATH)/keel-version-files "$$release_version" $O/root.patched || exit 1; \ else \ echo; \ echo "WARNING: can't tag local release (./changelog doesn't exist)"; \ diff --git a/mk/turnkey.mk b/mk/turnkey.mk index 4b581fa1..3a6fcddb 100644 --- a/mk/turnkey.mk +++ b/mk/turnkey.mk @@ -73,7 +73,8 @@ define _root.patched/post # @if [ -f ./changelog ]; then \ release_version=$$($(FAB_SHARE_PATH)/turnkey-version.py --dist=$(CODENAME) --tag=$(VERSION_TAG) ./changelog $(FAB_ARCH)); \ - $(COMMON_BIN_PATH)/keel-version-files $$release_version $O/root.patched || exit 1; \ + [ -x $(COMMON_BIN_PATH)/keel-version-files ] || { echo "ERROR: $(COMMON_BIN_PATH)/keel-version-files is missing or not executable: the common checkout predates the identity files of decision 0014, update it" >&2; exit 1; }; \ + $(COMMON_BIN_PATH)/keel-version-files "$$release_version" $O/root.patched || exit 1; \ else \ echo; \ echo "WARNING: can't tag local release (./changelog doesn't exist)"; \ diff --git a/tests/coverage.sh b/tests/coverage.sh index bbb53498..c0df0ec2 100755 --- a/tests/coverage.sh +++ b/tests/coverage.sh @@ -77,6 +77,18 @@ done # and pam-unix.bats pins the pam_unix behaviour the others rest on. bats "$root/tests/before-firstboot.bats" "$root/tests/pam-unix.bats" +# Suites whose subject kcov cannot measure: the make recipes of mk/, run by +# make against stubs of fab. They must pass; they contribute no percentage. +unmeasured=( + tests/mk-identity.bats +) +for suite in "${unmeasured[@]}"; do + if ! bats "$root/$suite"; then + echo "$suite: failed" >&2 + failed=1 + fi +done + if [ "$failed" -ne 0 ]; then exit 1 fi diff --git a/tests/mk-identity.bats b/tests/mk-identity.bats new file mode 100644 index 00000000..c50f7e88 --- /dev/null +++ b/tests/mk-identity.bats @@ -0,0 +1,89 @@ +#!/usr/bin/env bats +# The root.patched/post recipe of mk/turnkey.mk and mk/turnkey-desktop.mk, +# run by make rather than read: the step that writes the two identity files +# of an image (decision 0014) through bin/keel-version-files. +# +# fab is replaced by stubs under tests/mk/: an empty product.mk, a +# turnkey-version.py that prints the version string a build would derive, +# a make-release-deb.py and a fab-chroot that only log. What is real is the +# makefile text under test, make, and bin/keel-version-files. + +bats_require_minimum_version 1.5.0 + +setup() { + ROOT="$(cd "$BATS_TEST_DIRNAME/.." && pwd)" + STUBS="$BATS_TEST_DIRNAME/mk" + WORK="$BATS_TEST_TMPDIR/product" + OUT="$BATS_TEST_TMPDIR/build" + mkdir -p "$WORK" "$OUT/root.patched/etc/apt/apt.conf.d" + : > "$WORK/changelog" + export PATH="$STUBS:$PATH" + export KEEL_TEST_RELEASE=turnkey-core-19.0-trixie-amd64 +} + +# run_post MAKEFILE [VAR=VALUE ...]: make the root.patched/post recipe of +# one of the two shared makefiles, from a product directory. +run_post() { + local mk=$1 + shift + run make --no-print-directory -C "$WORK" -f "$STUBS/harness.mk" \ + MK="$ROOT/mk/$mk" FAB_SHARE_PATH="$STUBS" O="$OUT" \ + FAB_PATH="$BATS_TEST_TMPDIR/fab" COMMON_BIN_PATH="$ROOT/bin" \ + CODENAME=trixie FAB_ARCH=amd64 "$@" post +} + +@test "turnkey.mk writes both identity files" { + run_post turnkey.mk + [ "$status" -eq 0 ] + [ "$(cat "$OUT/root.patched/etc/turnkey_version")" = turnkey-core-19.0-trixie-amd64 ] + [ "$(cat "$OUT/root.patched/etc/keel_version")" = keel-core-19.0-trixie-amd64 ] +} + +@test "turnkey-desktop.mk writes both identity files too" { + run_post turnkey-desktop.mk + [ "$status" -eq 0 ] + [ "$(cat "$OUT/root.patched/etc/turnkey_version")" = turnkey-core-19.0-trixie-amd64 ] + [ "$(cat "$OUT/root.patched/etc/keel_version")" = keel-core-19.0-trixie-amd64 ] +} + +@test "a keel- changelog name still gives the compatibility file its prefix" { + KEEL_TEST_RELEASE=keel-core-19.0-trixie-amd64 run_post turnkey.mk + [ "$status" -eq 0 ] + [ "$(cat "$OUT/root.patched/etc/turnkey_version")" = turnkey-core-19.0-trixie-amd64 ] +} + +@test "turnkey.mk: a missing script stops the build and names the stale checkout" { + run_post turnkey.mk COMMON_BIN_PATH="$BATS_TEST_TMPDIR/nowhere" + [ "$status" -ne 0 ] + [[ "$output" == *"$BATS_TEST_TMPDIR/nowhere/keel-version-files"* ]] + [[ "$output" == *"predates"* ]] + [ ! -e "$OUT/root.patched/etc/turnkey_version" ] +} + +@test "turnkey-desktop.mk: a missing script stops the build and names the stale checkout" { + run_post turnkey-desktop.mk COMMON_BIN_PATH="$BATS_TEST_TMPDIR/nowhere" + [ "$status" -ne 0 ] + [[ "$output" == *"$BATS_TEST_TMPDIR/nowhere/keel-version-files"* ]] + [[ "$output" == *"predates"* ]] +} + +@test "an empty version string is one argument, and is refused as a version" { + KEEL_TEST_RELEASE="" run_post turnkey.mk + [ "$status" -ne 0 ] + [[ "$output" == *"is not an appliance identity"* ]] + [[ "$output" != *"two arguments are required"* ]] +} + +@test "a version string with a space in it is one argument, and is refused as a version" { + KEEL_TEST_RELEASE="turnkey-core 19.0-trixie-amd64" run_post turnkey.mk + [ "$status" -ne 0 ] + [[ "$output" == *"is not an appliance identity"* ]] +} + +@test "without a changelog nothing is written and the build goes on" { + rm "$WORK/changelog" + run_post turnkey.mk + [ "$status" -eq 0 ] + [[ "$output" == *"can't tag local release"* ]] + [ ! -e "$OUT/root.patched/etc/keel_version" ] +} diff --git a/tests/mk/fab-chroot b/tests/mk/fab-chroot new file mode 100755 index 00000000..4a739b4e --- /dev/null +++ b/tests/mk/fab-chroot @@ -0,0 +1,3 @@ +#!/bin/sh +# Stub of fab-chroot: logs, enters nothing. +echo "fab-chroot $*" diff --git a/tests/mk/harness.mk b/tests/mk/harness.mk new file mode 100644 index 00000000..3e3641cd --- /dev/null +++ b/tests/mk/harness.mk @@ -0,0 +1,8 @@ +# Runs the root.patched/post recipe of one shared makefile, MK, the way +# fab's product.mk runs it. FAB_SHARE_PATH points at this directory, so the +# product.mk that MK includes is the empty one here. +include $(MK) + +.PHONY: post +post: + $(root.patched/post) diff --git a/tests/mk/make-release-deb.py b/tests/mk/make-release-deb.py new file mode 100755 index 00000000..8675c11f --- /dev/null +++ b/tests/mk/make-release-deb.py @@ -0,0 +1,3 @@ +#!/bin/sh +# Stub of fab's make-release-deb.py: logs, builds nothing. +echo "make-release-deb.py $*" diff --git a/tests/mk/product.mk b/tests/mk/product.mk new file mode 100644 index 00000000..e69de29b diff --git a/tests/mk/turnkey-version.py b/tests/mk/turnkey-version.py new file mode 100755 index 00000000..5d2bd194 --- /dev/null +++ b/tests/mk/turnkey-version.py @@ -0,0 +1,4 @@ +#!/bin/sh +# Stub of fab's turnkey-version.py: prints the version string a build +# derives from the product changelog, taken from KEEL_TEST_RELEASE. +printf '%s\n' "$KEEL_TEST_RELEASE" From e67c0468d9a5dcb637725dd22c78b8ee3c116670 Mon Sep 17 00:00:00 2001 From: navigator Date: Tue, 29 Sep 2026 03:22:03 +0000 Subject: [PATCH 3/4] refactor: one prefix rule for both helpers, and say what a tag may be kvf_app_version and kvf_is_app_version each walked KVF_PREFIXES to answer the same question with opposite senses; kvf_has_prefix is that question. A hyphenated VERSION_TAG gives the version string a fifth field and is refused. The usage now says the tag joins the version field, so that is not found for the first time during a release. --- bin/keel-version-files | 4 ++++ lib/version-files.sh | 35 ++++++++++++++++++++++------------- tests/version-files.bats | 29 +++++++++++++++++++++++++++++ 3 files changed, 55 insertions(+), 13 deletions(-) diff --git a/bin/keel-version-files b/bin/keel-version-files index de88e257..9132b1f6 100755 --- a/bin/keel-version-files +++ b/bin/keel-version-files @@ -27,6 +27,10 @@ RELEASE_VERSION, the string turnkey-version.py builds from the product changelog (turnkey-core-19.0-trixie-amd64, keel-core-19.0-trixie-amd64). Whatever prefix it carries, the compatibility file is written with the turnkey- prefix its parsers require and the Keel file with keel-. + +The release tag a build passes as VERSION_TAG is appended to the version +field, so it must contain no hyphen: --tag=rc gives core-19.0rc-trixie-amd64 +and is accepted, --tag=-rc1 gives a fifth field and is refused. USAGE } diff --git a/lib/version-files.sh b/lib/version-files.sh index 43ff7192..252102e5 100644 --- a/lib/version-files.sh +++ b/lib/version-files.sh @@ -30,6 +30,20 @@ KVF_PREFIXES="turnkey keel" KVF_TURNKEY_PREFIX=turnkey KVF_KEEL_PREFIX=keel +# kvf_has_prefix TEXT +# TEXT begins with one of the product prefixes and its hyphen. The one rule +# both helpers below apply: kvf_app_version removes such a prefix, and +# kvf_is_app_version refuses a string that still carries one. +kvf_has_prefix() { + local text=${1-} prefix + for prefix in $KVF_PREFIXES; do + if [ "${text#"$prefix"-}" != "$text" ]; then + return 0 + fi + done + return 1 +} + # kvf_app_version RELEASE_NAME # The --- part of a release version string, # with at most one product prefix removed: turnkey-wordpress-19.0-trixie-amd64 @@ -37,13 +51,10 @@ KVF_KEEL_PREFIX=keel # neither prefix is returned whole, and an app whose own name starts with # the other product's name keeps it. kvf_app_version() { - local text=${1-} prefix - for prefix in $KVF_PREFIXES; do - if [ "${text#"$prefix"-}" != "$text" ]; then - printf '%s\n' "${text#"$prefix"-}" - return 0 - fi - done + local text=${1-} + if kvf_has_prefix "$text"; then + text=${text#*-} + fi printf '%s\n' "$text" } @@ -55,12 +66,10 @@ kvf_app_version() { # that still carries a product prefix fails, because "turnkey" would then # be read as the app: the prefix comes off first, with kvf_app_version. kvf_is_app_version() { - local text=${1-} prefix - for prefix in $KVF_PREFIXES; do - if [ "${text#"$prefix"-}" != "$text" ]; then - return 1 - fi - done + local text=${1-} + if kvf_has_prefix "$text"; then + return 1 + fi [[ $text =~ ^[a-z0-9][a-z0-9.+-]*-[0-9][^-]*-[a-z][a-z0-9]*-[a-z0-9]+$ ]] } diff --git a/tests/version-files.bats b/tests/version-files.bats index 8dccbcfd..38323765 100644 --- a/tests/version-files.bats +++ b/tests/version-files.bats @@ -59,6 +59,27 @@ setup() { [ -z "$output" ] } +# the one prefix rule both helpers apply + +@test "has_prefix: each product prefix followed by a hyphen" { + kvf_has_prefix turnkey-core-19.0-trixie-amd64 + kvf_has_prefix keel-core-19.0-trixie-amd64 +} + +@test "has_prefix: a prefix without its hyphen is not one" { + run kvf_has_prefix keelson-19.0-trixie-amd64 + [ "$status" -eq 1 ] + run kvf_has_prefix turnkey + [ "$status" -eq 1 ] +} + +@test "has_prefix: an empty string and no argument have none" { + run kvf_has_prefix "" + [ "$status" -eq 1 ] + run kvf_has_prefix + [ "$status" -eq 1 ] +} + # the grammar of the four fields @test "is_app_version: the four fields of an appliance" { @@ -195,6 +216,14 @@ setup() { [[ $output == *"usage: keel-version-files"* ]] } +@test "script: a hyphenated release tag is refused, and the usage says why" { + run "$SCRIPT" turnkey-core-19.0-rc1-trixie-amd64 "$SCRATCH" + [ "$status" -eq 1 ] + run "$SCRIPT" -h + [[ $output == *"VERSION_TAG"* ]] + [[ $output == *"no hyphen"* ]] +} + @test "script: -h prints the usage and succeeds" { run "$SCRIPT" -h [ "$status" -eq 0 ] From de7f4100eecafe2eae98ceaa374d0d880184e270 Mon Sep 17 00:00:00 2001 From: navigator Date: Fri, 2 Oct 2026 18:54:01 +0000 Subject: [PATCH 4/4] test: the mk identity recipe seals root as 19.x does; changelog entry Rebased onto 19.x after common#30 and #31. The root.patched/post recipe of mk/turnkey.mk now ends in mk/turnkey/seal-root (#31), which the fab stubs of tests/mk-identity.bats did not reach, so three of its tests failed. The harness links the real seal-root under its FAB_PATH and gives the scratch root a locked root, and asserts what both changes promise: both identity files, the build date stamped last, a shipped root password refused, and no per-appliance apt User-Agent written (#6), which this branch's makefiles used to write before the rebase. The feature gets its bullet in the unreleased changelog entry. --- changes/turnkey.changelog | 15 ++++++++++++++ tests/apt-sources.bats | 11 +++++++--- tests/mk-identity.bats | 42 ++++++++++++++++++++++++++++++++++++--- 3 files changed, 62 insertions(+), 6 deletions(-) diff --git a/changes/turnkey.changelog b/changes/turnkey.changelog index b2e365ea..9a456491 100644 --- a/changes/turnkey.changelog +++ b/changes/turnkey.changelog @@ -1,5 +1,20 @@ turnkey-core-19.0 (1) turnkey; urgency=low + * An image says what it is: /etc/keel_version, written beside + /etc/turnkey_version in root.patched/post by bin/keel-version-files + (decision 0014). Both carry the same four fields, app, version, + codename and architecture, from the first line of the product + changelog through fab's turnkey-version.py; the Keel file begins + keel-, the compatibility file always begins turnkey-, whatever the + changelog's release package is called, because sysversion, + turnkey-version, 29tagid and keel inspect parse it by that prefix. + Keel's turnkey-version fork reads /etc/keel_version first. Desktop + builds (mk/turnkey-desktop.mk) write both too, and a common checkout + without the script stops the build naming it, instead of dying with + "No such file" after the whole root was built. tests/version-files.bats + and tests/mk-identity.bats, which makes the recipe of both makefiles + against stubs of fab, with the real seal-root last. + * An image is exported with root locked, or the build fails. The last step of root.patched, mk/turnkey/seal-root, accepts a root password field of '*', '!' or '!*' and nothing else, without printing it, then diff --git a/tests/apt-sources.bats b/tests/apt-sources.bats index a2ee75a0..9a418a76 100644 --- a/tests/apt-sources.bats +++ b/tests/apt-sources.bats @@ -236,9 +236,14 @@ fetch_hosts() { run ! grep -qE "^$name([[:space:]]|\$)" "$REPO/plans/turnkey/base" done run ! grep -q 'make-release-deb' "$REPO/mk/turnkey.mk" - # the compatibility file is still written (decision 0014) - grep -q 'turnkey_version=.*turnkey-version.py' "$REPO/mk/turnkey.mk" - grep -q '> \$O/root.patched/etc/turnkey_version' "$REPO/mk/turnkey.mk" + # the compatibility file is still written (decision 0014), by + # bin/keel-version-files beside /etc/keel_version, from the version + # turnkey-version.py derives; tests/mk-identity.bats makes the recipe + # against stubs of fab and reads both files back + grep -q 'release_version=.*turnkey-version.py' "$REPO/mk/turnkey.mk" + # the $ are make's, matched literally + # shellcheck disable=SC2016 + grep -q 'keel-version-files "\$\$release_version" \$O/root.patched' "$REPO/mk/turnkey.mk" } # ------------------------------------------------ the security-only upgrade diff --git a/tests/mk-identity.bats b/tests/mk-identity.bats index c50f7e88..8e63b35b 100644 --- a/tests/mk-identity.bats +++ b/tests/mk-identity.bats @@ -6,7 +6,10 @@ # fab is replaced by stubs under tests/mk/: an empty product.mk, a # turnkey-version.py that prints the version string a build would derive, # a make-release-deb.py and a fab-chroot that only log. What is real is the -# makefile text under test, make, and bin/keel-version-files. +# makefile text under test, make, bin/keel-version-files, and +# mk/turnkey/seal-root, the last step of the same recipe, reached through +# the FAB_PATH the recipe names it by; the scratch root ships root locked, +# as an image must (common#31). bats_require_minimum_version 1.5.0 @@ -15,7 +18,10 @@ setup() { STUBS="$BATS_TEST_DIRNAME/mk" WORK="$BATS_TEST_TMPDIR/product" OUT="$BATS_TEST_TMPDIR/build" - mkdir -p "$WORK" "$OUT/root.patched/etc/apt/apt.conf.d" + FAB="$BATS_TEST_TMPDIR/fab" + mkdir -p "$WORK" "$OUT/root.patched/etc/apt/apt.conf.d" "$FAB/common/mk/turnkey" + ln -s "$ROOT/mk/turnkey/seal-root" "$FAB/common/mk/turnkey/seal-root" + printf 'root:*:20718:0:99999:7:::\n' > "$OUT/root.patched/etc/shadow" : > "$WORK/changelog" export PATH="$STUBS:$PATH" export KEEL_TEST_RELEASE=turnkey-core-19.0-trixie-amd64 @@ -28,7 +34,7 @@ run_post() { shift run make --no-print-directory -C "$WORK" -f "$STUBS/harness.mk" \ MK="$ROOT/mk/$mk" FAB_SHARE_PATH="$STUBS" O="$OUT" \ - FAB_PATH="$BATS_TEST_TMPDIR/fab" COMMON_BIN_PATH="$ROOT/bin" \ + FAB_PATH="$FAB" COMMON_BIN_PATH="$ROOT/bin" \ CODENAME=trixie FAB_ARCH=amd64 "$@" post } @@ -39,6 +45,36 @@ run_post() { [ "$(cat "$OUT/root.patched/etc/keel_version")" = keel-core-19.0-trixie-amd64 ] } +@test "turnkey.mk still seals root last: the build date is stamped after the identity files" { + run_post turnkey.mk + [ "$status" -eq 0 ] + [ "$(cat "$OUT/root.patched/etc/keel/build-date")" = "$(date -u +%F)" ] + [ -s "$OUT/root.patched/etc/keel_version" ] +} + +@test "turnkey.mk writes no per-appliance apt User-Agent (common#6)" { + # the identity files used to come with /etc/apt/apt.conf.d/01turnkey, + # which told every archive which appliance this is; the overlay's 01keel + # is the header now, and conf/turnkey.d/apt-identity removes a 01turnkey + run_post turnkey.mk + [ "$status" -eq 0 ] + [ -z "$(ls -A "$OUT/root.patched/etc/apt/apt.conf.d")" ] + run_post turnkey-desktop.mk + [ "$status" -eq 0 ] + [ -z "$(ls -A "$OUT/root.patched/etc/apt/apt.conf.d")" ] +} + +@test "turnkey.mk: a root that ships a password fails the build after the identity files" { + # a yescrypt field, literal: the $ are the hash's, not the shell's + # shellcheck disable=SC2016 + printf 'root:$y$j9T$abcdefghijklmnop$qrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123:20718:0:99999:7:::\n' \ + > "$OUT/root.patched/etc/shadow" + run_post turnkey.mk + [ "$status" -ne 0 ] + [[ "$output" == *"root has a password"* ]] + [ ! -e "$OUT/root.patched/etc/keel/build-date" ] +} + @test "turnkey-desktop.mk writes both identity files too" { run_post turnkey-desktop.mk [ "$status" -eq 0 ]