Files
Panama/tests/setup/apps-contract
T
Gabriel Brown 99156442b5 Merge branch 'main' into codex/repo-audit-remediation-package-2
# Conflicts:
#	README.md
#	setup/scripts/install-packages
2026-08-27 16:51:53 -04:00

116 lines
5.2 KiB
Bash
Executable File

#!/usr/bin/env bash
# The applications built from source, and the standard they have to meet.
#
# Every other application Panama installs comes from dnf or Flathub. These do
# not, and the rule for admitting one is not "it was convenient": there has to
# be no packaged form, the reason has to be written down, and what lands on the
# system still has to be a package the system owns.
#
# The rules:
#
# 1. Every definition declares a repository, a description, and a build.
# A file missing any of them is an entry that fails only when somebody
# asks for it, which is the worst moment to find out.
# 2. Every definition states why the exception exists. This is the whole
# guard against the list growing by habit -- sunhat had seventy-odd
# installers and no reason recorded for any of them.
# 3. Nothing is pinned. A recorded version is a 404 waiting to happen: every
# pinned URL sunhat carried had rotted within a release cycle, which is the
# argument this repository's package rule is built on.
# 4. `panama app` lists what the directory holds and refuses what it does not.
#
# Definitions are read, not run. Building one downloads an upstream release and
# installs a package, which is not something a test suite does.
set -uo pipefail
repo_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
apps_dir="$repo_dir/setup/apps"
panama="$repo_dir/bin/panama"
findings=()
note() { findings+=("$1"); }
shopt -s nullglob
definitions=("$apps_dir"/*)
# ── 1 & 2. Each definition is complete, and says why it exists ───────────────
for definition in "${definitions[@]}"; do
name="$(basename "$definition")"
[[ -f "$definition" ]] || { note "$name is not a file"; continue; }
# Sourced in a subshell so one definition cannot leak into the next, and so
# a definition that runs something at source time is contained.
problems="$(
description=""
repo=""
unset -f build 2>/dev/null || true
# shellcheck source=/dev/null
source "$definition" >/dev/null 2>&1
[[ -n "$description" ]] || { printf 'no-description\n'; exit 0; }
[[ -n "$repo" ]] || { printf 'no-repo\n'; exit 0; }
declare -F build >/dev/null || { printf 'no-build\n'; exit 0; }
[[ "$repo" == https://* ]] || { printf 'insecure-repo\n'; exit 0; }
)"
# Read back through a here-string rather than a pipe: a `while read` on the
# right of a pipe runs in a subshell, and every finding it recorded was
# being discarded at the end of the loop. Caught by standing in a broken
# definition and watching two of the three checks stay silent.
while read -r problem; do
[[ -n "$problem" ]] || continue
case "$problem" in
no-description) note "$name has no description, so it cannot be listed" ;;
no-repo) note "$name declares no repository" ;;
no-build) note "$name declares no build function" ;;
insecure-repo) note "$name is cloned over something other than https" ;;
esac
done <<<"$problems"
# The comment block is the reason. A definition without one is an entry
# somebody added because it was easy.
reason="$(grep -c '^#' "$definition")"
(( reason >= 3 )) \
|| note "$name records no reason for being a source build rather than a package"
# ── 3. Nothing pinned ───────────────────────────────────────────────────
if grep -qE 'git (checkout|clone).*(-b|--branch|--tag)|checkout [0-9a-f]{7,40}|v[0-9]+\.[0-9]+\.[0-9]+' "$definition"; then
note "$name looks like it pins a version or tag, which is what goes stale"
fi
done
# ── 4. The command agrees with the directory ────────────────────────────────
listing="$("$panama" app 2>&1)"
for definition in "${definitions[@]}"; do
[[ -f "$definition" ]] || continue
grep -q "$(basename "$definition")" <<<"$listing" \
|| note "$(basename "$definition") is not listed by 'panama app'"
done
"$panama" app definitely-not-an-app >/dev/null 2>&1 \
&& note "'panama app' accepts a name that has no definition"
# The build tree belongs in the cache: it is entirely rebuildable, and a
# checkout kept beside the repository would eventually be mistaken for one.
grep -q 'XDG_CACHE_HOME' "$panama" \
|| note 'application checkouts are not placed under the cache directory'
# Not part of the unattended run, for the reason the interview exists.
grep -q 'app)' "$repo_dir/install" \
&& note 'the installer runs a source build, which cannot be walked away from'
# ── Report ───────────────────────────────────────────────────────────────────
if (( ${#findings[@]} > 0 )); then
mapfile -t findings < <(printf '%s\n' "${findings[@]}" | sort -u)
printf 'apps contract: %d finding(s)\n' "${#findings[@]}" >&2
printf ' - %s\n' "${findings[@]}" >&2
exit 1
fi
printf 'apps contract: PASS (%d applications)\n' "${#definitions[@]}"