Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 34 additions & 1 deletion COVERAGE.md
Original file line number Diff line number Diff line change
Expand Up @@ -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, 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. 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.
Expand Down Expand Up @@ -193,6 +195,37 @@ 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` 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
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.

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
Expand Down
91 changes: 91 additions & 0 deletions bin/keel-version-files
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
#!/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 <<USAGE
usage: keel-version-files RELEASE_VERSION ROOTDIR

Writes ROOTDIR/etc/turnkey_version and ROOTDIR/etc/keel_version from
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
}

case "${1-}" in
-h|--help)
usage
exit 0
;;
esac

if [ "$#" -ne 2 ]; then
usage >&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
15 changes: 15 additions & 0 deletions changes/turnkey.changelog
Original file line number Diff line number Diff line change
@@ -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
Expand Down
80 changes: 80 additions & 0 deletions lib/version-files.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
#!/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-<app>-<version>-<codename>-<arch>,
# 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_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 <app>-<version>-<codename>-<arch> 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-}
if kvf_has_prefix "$text"; then
text=${text#*-}
fi
printf '%s\n' "$text"
}

# kvf_is_app_version TEXT
# TEXT carries the four fields an appliance identity needs:
# <app>-<version>-<codename>-<arch>, 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-}
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]+$ ]]
}

# 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-}"
}
13 changes: 9 additions & 4 deletions mk/turnkey-desktop.mk
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -47,15 +50,16 @@ 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).
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; \
Expand All @@ -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)"; \
Expand Down
13 changes: 9 additions & 4 deletions mk/turnkey.mk
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand All @@ -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-<app>-<version>) is no longer built:
# keel-core is the meta package of a Keel image (handbook decision 0047),
Expand All @@ -65,11 +69,12 @@ 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)); \
[ -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)"; \
Expand Down
11 changes: 8 additions & 3 deletions tests/apt-sources.bats
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
14 changes: 14 additions & 0 deletions tests/coverage.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -75,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
Loading
Loading