Files
fips/testing/check-package-versions.sh
T
Johnathan Corgan 86fdb99890 Generalise the Debian version check into a package version check
Rename testing/check-deb-version.sh to check-package-versions.sh and
restructure it so the workflow extraction, the derivation run and the
assertions take the workflow, job and step names as arguments. The
Debian cases and wiring checks are unchanged in substance; the
structure lets other packaging workflows' version derivations be
checked by the same script.

One change in exit semantics: a derivation step that runs cleanly but
does not write a declared output is now a failed check (exit 1) rather
than "could not run" (exit 2), because the harness did run and it is
the workflow that is wrong. An empty version is also refused before it
reaches dpkg, which would otherwise order it below everything, and a
dpkg exit other than 0 or 1 is reported as "could not compare".

ci.yml and ci-local.sh call the renamed script under the
package-versions name.
2026-10-01 22:41:14 +00:00

210 lines
8.6 KiB
Bash
Executable File

#!/bin/bash
# ── Package version derivation check ────────────────────────────────────────
# A release candidate is tagged vX.Y.Z-rcN, because git refuses '~' in a ref
# name. dpkg reads X.Y.Z-rcN as revision rcN of X.Y.Z and sorts it ABOVE the
# release, so a host that installed the candidate is never upgraded by the
# release. package-linux.yml's "Derive Linux package version" step therefore
# maps the tag's pre-release suffix to '~' for the .deb, which dpkg sorts below.
#
# This runs that step's own text, taken from the workflow, rather than a copy,
# so the check and the workflow cannot drift apart.
#
# Debian cases:
# refs/tags/v0.5.3 both versions 0.5.3
# refs/tags/v0.5.3-rc1 deb 0.5.3~rc1, tarball and artifact 0.5.3-rc1, and
# dpkg orders 0.5.2 < 0.5.3~rc1 < 0.5.3
# refs/heads/maint deb equals the tarball version, which is
# <Cargo version>+maint.<height>.<hash>
#
# It also checks the wiring the derivation needs to have any effect: the job
# declares the deb_package_version output, the .deb build passes it as
# --version, and that step renames a '~' in the package file name to '-' (a
# GitHub release renames an asset with special characters in its name, which
# would leave checksums-linux.txt naming a file the release does not have).
# Those three are read from the text, not executed.
#
# Exit 0 = clean. Exit 1 = a case or a wiring check failed, including a
# derivation that ran but did not write a declared output. Exit 2 = the check
# could not run (no dpkg, no PyYAML, a step not found, the derivation failed,
# or dpkg could not compare); never treated as a pass.
# ─────────────────────────────────────────────────────────────────────────────
set -uo pipefail
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
PROJECT_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
WORKFLOWS="$PROJECT_ROOT/.github/workflows"
cant() {
echo "check-package-versions: $*; cannot verify the package version derivation" >&2
exit 2
}
command -v dpkg >/dev/null 2>&1 || cant "dpkg not found"
command -v python3 >/dev/null 2>&1 || cant "python3 not found"
python3 -c "import yaml" >/dev/null 2>&1 || cant "python3 module 'yaml' not found"
WORK=$(mktemp -d) || cant "mktemp failed"
trap 'rm -rf "$WORK"' EXIT
# Pull a derivation step's run text, its job's declared outputs, and a build
# step's env block and run text out of a workflow, into
# $WORK/<prefix>.derive.sh, <prefix>.outputs and <prefix>.build.sh.
# Usage: extract <prefix> <workflow file> <job> <derive step> <build job> <build step>
extract() {
local prefix="$1" workflow="$WORKFLOWS/$2"
[ -f "$workflow" ] || cant "missing $workflow"
python3 - "$workflow" "$WORK" "$prefix" "$3" "$4" "$5" "$6" <<'PY' || exit 2
import sys
from pathlib import Path
import yaml
workflow, work, prefix, job, derive_name, build_job, build_name = sys.argv[1:]
work = Path(work)
doc = yaml.safe_load(Path(workflow).read_text(encoding="utf-8"))
jobs = doc.get("jobs") or {}
def find_step(job, name):
for step in (jobs.get(job) or {}).get("steps") or []:
if step.get("name") == name:
return step
return None
derive = find_step(job, derive_name)
build = find_step(build_job, build_name)
for label, step in (("derivation", derive), ("build", build)):
if not step or not step.get("run"):
print(f"check-package-versions: the {label} step was not found in {workflow}",
file=sys.stderr)
sys.exit(2)
(work / f"{prefix}.derive.sh").write_text(derive["run"], encoding="utf-8")
env = build.get("env") or {}
(work / f"{prefix}.build.sh").write_text(
"".join(f"{k}: {v}\n" for k, v in env.items()) + build["run"],
encoding="utf-8")
outputs = (jobs.get(job) or {}).get("outputs") or {}
(work / f"{prefix}.outputs").write_text(
"".join(f"{k}={v}\n" for k, v in outputs.items()), encoding="utf-8")
PY
}
FAILED=0
ok() { echo " PASS $*"; }
bad() { echo " FAIL $*"; FAILED=$((FAILED + 1)); }
# Run a derivation step for one ref and load the named outputs into OUT.
# Exits 2 when the step itself fails: no version was established. A step that
# runs but does not write a declared output is a failed check rather than a
# harness failure, so it is recorded and the output is left empty.
# Usage: derive <prefix> <ref> <output name>...
declare -A OUT
derive() {
local prefix="$1" ref="$2" out="$WORK/github_output" name
shift 2
: > "$out"
if ! (cd "$PROJECT_ROOT" && GITHUB_OUTPUT="$out" GITHUB_REF="$ref" \
GITHUB_REF_NAME="${ref#refs/*/}" bash -eo pipefail "$WORK/$prefix.derive.sh") >"$WORK/derive.log" 2>&1; then
echo "check-package-versions: the $prefix derivation failed for $ref:" >&2
cat "$WORK/derive.log" >&2
exit 2
fi
for name in "$@"; do
OUT[$name]=$(sed -n "s/^$name=//p" "$out")
if [ -z "${OUT[$name]}" ]; then
bad "the $prefix derivation for $ref did not write $name"
fi
done
return 0
}
# Record whether dpkg orders $1 $2 $3 (lt, gt, ...). An empty version is a
# failed case, because dpkg would compare it and sort it below everything.
expect_order() {
local rc=0
if [ -z "$1" ] || [ -z "$3" ]; then
bad "dpkg: no version to compare ('$1' $2 '$3')"
return 0
fi
dpkg --compare-versions "$1" "$2" "$3" 2>"$WORK/dpkg.err" || rc=$?
case $rc in
0) ok "dpkg: $1 $2 $3" ;;
1) bad "dpkg: $1 $2 $3 does not hold" ;;
*) cat "$WORK/dpkg.err" >&2
cant "dpkg could not compare $1 $2 $3 (exit $rc)" ;;
esac
return 0
}
# Record whether a derived version equals the expected one.
expect_eq() {
local label="$1" got="$2" want="$3"
if [ "$got" = "$want" ]; then
ok "$label: $got"
else
bad "$label: '$got' (want $want)"
fi
return 0
}
# Record whether extracted workflow text contains a fixed string. With -x the
# string must be a whole line.
# Usage: expect_text [-x] <file> <text> <description>
expect_text() {
local flags=-qF
if [ "$1" = -x ]; then flags=-qxF; shift; fi
if grep "$flags" -- "$2" "$1"; then
ok "$3"
else
bad "not so: $3"
fi
return 0
}
CRATE=$(awk -F'"' '/^version = /{print $2; exit}' "$PROJECT_ROOT/Cargo.toml")
HEIGHT=$(git -C "$PROJECT_ROOT" rev-list --count HEAD) || cant "git rev-list failed"
HASH=$(git -C "$PROJECT_ROOT" rev-parse --short HEAD) || cant "git rev-parse failed"
[ -n "$CRATE" ] || cant "no version in Cargo.toml"
# ── Debian .deb, package-linux.yml ──────────────────────────────────────────
extract deb package-linux.yml determine-versioning "Derive Linux package version" \
build "Build Debian package in the pinned container"
echo "Debian: release tag refs/tags/v0.5.3"
derive deb refs/tags/v0.5.3 linux_package_version deb_package_version
expect_eq "linux_package_version" "${OUT[linux_package_version]}" "0.5.3"
expect_eq "deb_package_version" "${OUT[deb_package_version]}" "0.5.3"
echo "Debian: candidate tag refs/tags/v0.5.3-rc1"
derive deb refs/tags/v0.5.3-rc1 linux_package_version deb_package_version
expect_eq "linux_package_version" "${OUT[linux_package_version]}" "0.5.3-rc1"
expect_eq "deb_package_version" "${OUT[deb_package_version]}" "0.5.3~rc1"
expect_order "${OUT[deb_package_version]}" lt 0.5.3
expect_order "${OUT[deb_package_version]}" gt 0.5.2
echo "Debian: branch refs/heads/maint"
derive deb refs/heads/maint linux_package_version deb_package_version
expect_eq "linux_package_version" "${OUT[linux_package_version]}" "${CRATE}+maint.${HEIGHT}.${HASH}"
expect_eq "deb_package_version" "${OUT[deb_package_version]}" "${OUT[linux_package_version]}"
echo "Debian: wiring"
# shellcheck disable=SC2016 # the ${{ }} expressions are workflow text, not shell
expect_text -x "$WORK/deb.outputs" \
'deb_package_version=${{ steps.linux_version.outputs.deb_package_version }}' \
"determine-versioning declares deb_package_version from the derivation step"
# shellcheck disable=SC2016
expect_text "$WORK/deb.build.sh" \
'--version "${{ needs.determine-versioning.outputs.deb_package_version }}"' \
"the .deb build passes deb_package_version as --version"
expect_text "$WORK/deb.build.sh" "tr '~' '-'" \
"the .deb build renames a '~' in the package file name"
echo
if [ "$FAILED" -eq 0 ]; then
echo "check-package-versions: all checks passed"
exit 0
fi
echo "check-package-versions: $FAILED check(s) failed"
exit 1