LibrePortal/scripts/cli/commands/artifact/cli_artifact_commands.sh
librelad 36a5c87397 fix(artifacts): propagate LP_INDEX_SIGSTATE to callers via lpFetchIndexInto
Every caller captured the index with var=$(lpFetchIndex), which runs the
fetch in a command-substitution subshell — the LP_INDEX_SIGSTATE global it
sets never reached the caller. On a box with real signing active the
artifactApply/apply-auto gates would therefore refuse a correctly signed
index (fail-closed, but the apply path would be dead on arrival the day
signing activates), and artifact index / the WebUI scan would report a
verified feed as UNSIGNED.

New lpFetchIndexInto <var> [cache] runs the fetch in the calling shell and
assigns via printf -v; all four call sites converted. Verified with a
source-and-mock harness against a locally served index: 10/10 (sigstate
reaches caller, serial high-water, anti-rollback refuse, staleness refuse,
id enumeration, envelope round-trip).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Signed-off-by: librelad <librelad@digitalangels.vip>
2026-07-03 20:43:59 +01:00

140 lines
5.5 KiB
Bash

#!/bin/bash
# Artifact command handler — `libreportal artifact <sub>`
# ---------------------------------------------------------------------------
# Dispatched automatically by cli_initialize.sh (category -> cliHandleArtifactCommands).
#
# The unified distribution primitive. The READ side (`index`/`applied`) fetches +
# verifies the team-signed artifact index (hotfixes today; apps/themes/components
# later — all one envelope) and lists what's available/applied; it changes nothing,
# so — like `updater check` — it runs directly. The state-changing `apply`/`revert`
# verbs route through the TASK system (snapshot → bounded declarative ops →
# auto-rollback → History), never a mutating API. See docs/roadmap/updates-and-
# distribution.md and cli_artifact_apply.sh.
cliHandleArtifactCommands()
{
local sub="$initial_command2"
local id="$initial_command3"
# Lazy-loader gap: ensure the read primitives + apply pipeline are defined.
# These are new files; the array/manifest regen self-heals them on deploy,
# but this covers the window before that. Source CHECKED — a missing/corrupt
# file must surface a clear error, not degrade to a bare "command not found".
if ! declare -F lpFetchIndex >/dev/null 2>&1; then
local _f
for _f in source/fetch.sh source/artifacts.sh; do
if [[ ! -f "$install_scripts_dir/$_f" ]] || ! source "$install_scripts_dir/$_f"; then
isError "artifact: failed to load the read pipeline ($_f) — try: libreportal regen"; return 1
fi
done
fi
if ! declare -F artifactApply >/dev/null 2>&1; then
local _af="cli/commands/artifact/cli_artifact_apply.sh"
if [[ ! -f "$install_scripts_dir/$_af" ]] || ! source "$install_scripts_dir/$_af"; then
isError "artifact: failed to load the apply pipeline ($_af) — try: libreportal regen"; return 1
fi
fi
case "$sub" in
""|"index"|"list")
artifactListIndex
;;
"applied")
artifactListApplied
;;
"apply")
if [[ -z "$id" ]]; then isError "Usage: libreportal artifact apply <id>"; return 1; fi
if [[ "$LIBREPORTAL_TASK_EXEC" == "1" ]]; then
artifactApply "$id"
else
cliTaskRun "libreportal artifact apply $id" "artifact_apply" "$id" ""
fi
;;
"revert"|"rollback")
if [[ -z "$id" ]]; then isError "Usage: libreportal artifact revert <id>"; return 1; fi
if [[ "$LIBREPORTAL_TASK_EXEC" == "1" ]]; then
artifactRevert "$id"
else
cliTaskRun "libreportal artifact revert $id" "artifact_revert" "$id" ""
fi
;;
"apply-auto")
# Decide + enqueue (gated by CFG_HOTFIX_AUTO); each apply is its own task.
artifactApplyAuto
;;
*)
cliShowArtifactHelp
;;
esac
}
# List the applied hotfixes (read-only) from the per-id applied records.
artifactListApplied()
{
isHeader "Applied hotfixes"
local dir; dir="${containers_dir%/}/libreportal/frontend/data/updater/generated/applied"
if ! compgen -G "$dir/*.json" >/dev/null 2>&1; then
isSuccessful "0 hotfixes applied."
return 0
fi
local n=0 f id title app
for f in "$dir"/*.json; do
n=$((n+1))
if command -v jq >/dev/null 2>&1; then
id="$(jq -r '.id' "$f" 2>/dev/null)"; title="$(jq -r '.title // ""' "$f" 2>/dev/null)"; app="$(jq -r '.app // ""' "$f" 2>/dev/null)"
echo "$id${app:+ [$app]}$title"
else
echo "$(basename "$f" .json)"
fi
done
isSuccessful "$n hotfix(es) applied. Revert with: libreportal artifact revert <id>"
}
# Fetch + verify the signed index and print a human summary. Read-only.
artifactListIndex()
{
isHeader "Artifact index ($(lpReleaseChannel))"
local json
if ! lpFetchIndexInto json; then
isError "Could not fetch or verify the artifact index from $(lpArtifactIndexUrl)."
isNotice "Nothing is published yet, or the channel is unreachable. (This is expected before the first index ships.)"
return 1
fi
local serial generated_at
serial="$(_lpJsonNum "$json" index_serial)"
generated_at="$(lpIndexTop generated_at "$json")"
# Report the ACTUAL signature state — never claim "verified" when the feed was
# only accepted because signing isn't activated (dev/placeholder key).
if [[ "$LP_INDEX_SIGSTATE" == "verified" ]]; then
isNotice "Signed + verified. serial=${serial:-?} generated=${generated_at:-?}"
else
isNotice "UNSIGNED (signing not activated — dev/placeholder key). serial=${serial:-?} generated=${generated_at:-?}"
fi
local ids; ids="$(lpIndexArtifactIds "$json")"
if [[ -z "$ids" ]]; then
isSuccessful "0 artifacts available — the index is empty (nothing to apply)."
return 0
fi
local n=0 id obj title type sev
while IFS= read -r id; do
[[ -z "$id" ]] && continue
n=$((n + 1))
obj="$(lpArtifactById "$json" "$id")"
if [[ -n "$obj" ]]; then
title="$(_lpJsonStr "$obj" title)"
type="$(_lpJsonStr "$obj" type)"
sev="$(_lpJsonStr "$obj" severity)"
echo " • [${type:-?}/${sev:-info}] $id${title:-}"
else
echo "$id"
fi
done <<< "$ids"
isSuccessful "$n artifact(s) available."
}