fix: make local recovery fail closed
This commit is contained in:
@@ -123,6 +123,10 @@ function requireTokens(label, text, tokens) {
|
||||
}
|
||||
}
|
||||
|
||||
function requirePattern(label, text, pattern) {
|
||||
if (!pattern.test(text)) throw new Error(label);
|
||||
}
|
||||
|
||||
let inCodeFence = false;
|
||||
for (const line of source.split(/\n/)) {
|
||||
if (line.trimStart().startsWith("```")) {
|
||||
@@ -151,18 +155,62 @@ requireTokens("native PowerShell health", healthPowerShell, [
|
||||
const updateShell = blocks("Update an installation", "sh").join("\n");
|
||||
requireTokens("installation-aware source update", updateShell, [
|
||||
"NEXT_PI_VERSION", "RUNNING_PI_VERSION", "--source build", "git rev-parse HEAD",
|
||||
"pi status", "status", "doctor", "curl --fail",
|
||||
"pi status", "status", "doctor", "curl --fail", "set -euo pipefail",
|
||||
"git status --porcelain --untracked-files=all", "USES_BASE_CORE", "thothii-core:local",
|
||||
]);
|
||||
if (!updateShell.includes("NEXT_PI_VERSION\" == \"$RUNNING_PI_VERSION") ||
|
||||
!updateShell.includes("exit 1")) {
|
||||
throw new Error("source update must fail closed when thothctl would no-op on the current Pi version");
|
||||
if (/\|\|\s*true|;\s*true\b/.test(updateShell)) throw new Error("POSIX source update contains a failure-bypass command");
|
||||
requirePattern("POSIX source update does not fail closed: source pull", updateShell,
|
||||
/if ! git pull --ff-only; then abort_update/);
|
||||
requirePattern("POSIX source update does not fail closed: installation status", updateShell,
|
||||
/if ! INSTALLATION_STATUS="\$\("\$THTCTL" --installation "\$INSTALLATION" status\)"; then/);
|
||||
requirePattern("POSIX source update does not fail closed: Pi status", updateShell,
|
||||
/if ! RUNNING_PI_VERSION="\$\("\$THTCTL" --installation "\$INSTALLATION" pi status\)"; then/);
|
||||
requirePattern("POSIX source update does not fail closed: local build", updateShell,
|
||||
/if ! bash scripts\/build-local\.sh; then/);
|
||||
requirePattern("POSIX source update does not fail closed: thothctl build", updateShell,
|
||||
/if ! bash scripts\/build-thothctl\.sh; then/);
|
||||
requirePattern("POSIX source update lacks the same-version/no-selector path", updateShell,
|
||||
/if \[\[ "\$NEXT_PI_VERSION" == "\$RUNNING_PI_VERSION" \]\]; then[\s\S]*"\$USES_BASE_CORE" == true[\s\S]*TRANSACTIONAL_PI_UPDATE=false/);
|
||||
for (const [label, pattern] of [
|
||||
["installation start", /if ! "\$THTCTL" --installation "\$INSTALLATION" start; then/],
|
||||
["frontend health", /if ! curl --fail http:\/\/127\.0\.0\.1:8080\/health; then/],
|
||||
["core health", /if ! curl --fail http:\/\/127\.0\.0\.1:8787\/health; then/],
|
||||
["final status", /if ! FINAL_STATUS="\$\("\$THTCTL" --installation "\$INSTALLATION" status\)"; then/],
|
||||
["final Pi status", /if ! FINAL_PI_STATUS="\$\("\$THTCTL" --installation "\$INSTALLATION" pi status\)"; then/],
|
||||
["final doctor", /if ! "\$THTCTL" --installation "\$INSTALLATION" doctor; then/],
|
||||
]) requirePattern(`POSIX source update does not fail closed: ${label}`, updateShell, pattern);
|
||||
const provenance = updateShell.indexOf("printf 'Built source revision:");
|
||||
if (provenance < updateShell.lastIndexOf("require_clean_source") ||
|
||||
provenance < updateShell.indexOf('abort_update "final doctor failed"')) {
|
||||
throw new Error("POSIX source revision provenance is printed before final checks");
|
||||
}
|
||||
|
||||
const updatePowerShell = blocks("Update an installation", "powershell").join("\n");
|
||||
requireTokens("native PowerShell source update", updatePowerShell, [
|
||||
"$NextPiVersion", "$RunningPiVersion", "--source build", "git rev-parse HEAD",
|
||||
"pi status", "status", "doctor", "curl.exe --fail", "throw",
|
||||
"pi status", "status", "doctor", "curl.exe --fail", "throw", "$ErrorActionPreference = 'Stop'",
|
||||
"git status --porcelain --untracked-files=all", "$UsesBaseCore", "thothii-core:local",
|
||||
"$TransactionalPiUpdate = $false",
|
||||
]);
|
||||
for (const [command, step] of [
|
||||
["git pull --ff-only", "source pull"],
|
||||
["$InstallationStatus = @(& $THTCTL --installation $INSTALLATION status)", "installation status"],
|
||||
["$RunningPiStatus = (& $THTCTL --installation $INSTALLATION pi status)", "Pi status"],
|
||||
["powershell -ExecutionPolicy Bypass -File scripts/build-local.ps1", "local image build"],
|
||||
["& \"C:\\Program Files\\Git\\bin\\bash.exe\" scripts/build-thothctl.sh", "thothctl build"],
|
||||
["curl.exe --fail --silent --show-error http://127.0.0.1:8080/health", "frontend health check"],
|
||||
["curl.exe --fail --silent --show-error http://127.0.0.1:8787/health", "core health check"],
|
||||
["$FinalPiStatus = (& $THTCTL --installation $INSTALLATION pi status)", "final Pi status"],
|
||||
["& $THTCTL --installation $INSTALLATION doctor", "final doctor"],
|
||||
]) {
|
||||
const commandAt = updatePowerShell.indexOf(command);
|
||||
const checkAt = updatePowerShell.indexOf(`Assert-NativeSuccess '${step}'`, commandAt);
|
||||
if (commandAt < 0 || checkAt < commandAt || checkAt - commandAt > 220) {
|
||||
throw new Error(`PowerShell source update does not propagate failure: ${step}`);
|
||||
}
|
||||
}
|
||||
requirePattern("PowerShell source update lacks the same-version/no-selector path", updatePowerShell,
|
||||
/if \(\$NextPiVersion -eq \$RunningPiVersion\) \{[\s\S]*-not \$UsesBaseCore[\s\S]*\$TransactionalPiUpdate = \$false/);
|
||||
|
||||
const backupPowerShell = blocks("Back up and restore", "powershell").join("\n");
|
||||
requireTokens("native PowerShell backup/restore", backupPowerShell, [
|
||||
@@ -176,6 +224,81 @@ for (const block of [setupPowerShell, healthPowerShell, updatePowerShell, backup
|
||||
}
|
||||
}
|
||||
NODE
|
||||
local update_fixture update_script fake_bin calls output status
|
||||
update_fixture="$(mktemp -d "${TMPDIR%/}/thoth-source-update.XXXXXX")"
|
||||
trap 'rm -rf "$update_fixture"' RETURN
|
||||
update_script="$update_fixture/update.sh"
|
||||
awk '
|
||||
/^## Update an installation$/ { in_section=1; next }
|
||||
in_section && /^```sh$/ { in_code=1; next }
|
||||
in_code && /^```$/ { exit }
|
||||
in_code { print }
|
||||
' "$guide" >"$update_script"
|
||||
chmod 0700 "$update_script"
|
||||
mkdir -p "$update_fixture/project/docker" "$update_fixture/project/scripts" "$update_fixture/bin"
|
||||
printf 'ARG PI_VERSION=0.80.3\n' >"$update_fixture/project/docker/core.Dockerfile"
|
||||
printf '%s\n' \
|
||||
'#!/bin/sh' \
|
||||
'printf "git %s\n" "$*" >>"$CALLS"' \
|
||||
'case "$1" in' \
|
||||
' status) if [ "$FAIL_STEP" = dirty ]; then printf "?? untracked-build-context\n"; fi ;;' \
|
||||
' pull) [ "$FAIL_STEP" != pull ] || exit 9 ;;' \
|
||||
' rev-parse) printf "0123456789abcdef\n" ;;' \
|
||||
'esac' \
|
||||
'exit 0' >"$update_fixture/bin/git"
|
||||
printf '%s\n' \
|
||||
'#!/bin/sh' \
|
||||
'printf "bash %s\n" "$*" >>"$CALLS"' \
|
||||
'if [ "$1" = scripts/build-local.sh ] && [ "$FAIL_STEP" = build ]; then exit 8; fi' \
|
||||
'exit 0' >"$update_fixture/bin/bash"
|
||||
printf '%s\n' \
|
||||
'#!/bin/sh' \
|
||||
'printf "thothctl %s\n" "$*" >>"$CALLS"' \
|
||||
'case " $* " in' \
|
||||
' *" pi status "*) [ "$FAIL_STEP" != status ] || exit 7; printf "Pi version: 0.80.3\n" ;;' \
|
||||
' *" status "*) printf "[{\"Service\":\"core\",\"Image\":\"thothii-core:local\"}]\n" ;;' \
|
||||
'esac' \
|
||||
'exit 0' >"$update_fixture/bin/thothctl"
|
||||
printf '%s\n' \
|
||||
'#!/bin/sh' \
|
||||
'printf "curl %s\n" "$*" >>"$CALLS"' \
|
||||
'exit 0' >"$update_fixture/bin/curl"
|
||||
chmod 0700 "$update_fixture/bin/git" "$update_fixture/bin/bash" \
|
||||
"$update_fixture/bin/thothctl" "$update_fixture/bin/curl"
|
||||
|
||||
for fixture_step in clean dirty pull status build; do
|
||||
calls="$update_fixture/calls-$fixture_step"
|
||||
output="$update_fixture/output-$fixture_step"
|
||||
: >"$calls"
|
||||
set +e
|
||||
(
|
||||
cd "$update_fixture/project"
|
||||
env PATH="$update_fixture/bin:$PATH" CALLS="$calls" FAIL_STEP="$fixture_step" \
|
||||
THTCTL="$update_fixture/bin/thothctl" INSTALLATION="$update_fixture/installation.yaml" \
|
||||
/bin/bash "$update_script"
|
||||
) >"$output" 2>&1
|
||||
status=$?
|
||||
set -e
|
||||
if [[ "$fixture_step" == clean ]]; then
|
||||
[[ $status -eq 0 ]] || { echo "same-version/no-selector source fixture failed" >&2; return 1; }
|
||||
grep -Fq 'Built source revision: 0123456789abcdef' "$output" || {
|
||||
echo "successful source fixture did not report revision provenance" >&2; return 1;
|
||||
}
|
||||
if grep -Fq ' pi update ' "$calls"; then
|
||||
echo "same-version/no-selector source fixture incorrectly invoked pi update" >&2
|
||||
return 1
|
||||
fi
|
||||
grep -Fq 'bash scripts/build-local.sh' "$calls" || return 1
|
||||
grep -Fq 'thothctl --installation ' "$calls" || return 1
|
||||
else
|
||||
[[ $status -ne 0 ]] || { echo "$fixture_step source failure fixture was accepted" >&2; return 1; }
|
||||
if grep -Fq 'Built source revision:' "$output"; then
|
||||
echo "$fixture_step source failure fixture claimed revision provenance" >&2
|
||||
return 1
|
||||
fi
|
||||
fi
|
||||
done
|
||||
echo "source update fail-closed semantics passed"
|
||||
echo "local installation guide contract passed"
|
||||
}
|
||||
|
||||
@@ -199,7 +322,12 @@ verify_windows_line_endings_guide() {
|
||||
"reclone"
|
||||
node - "$guide" <<'NODE'
|
||||
const fs = require("fs");
|
||||
const lines = fs.readFileSync(process.argv[2], "utf8").split(/\n/);
|
||||
const source = fs.readFileSync(process.argv[2], "utf8");
|
||||
const lines = source.split(/\n/);
|
||||
const sectionStart = source.indexOf("## Recover an existing CRLF clone");
|
||||
const recovery = source.slice(sectionStart);
|
||||
const shell = [...recovery.matchAll(/```sh\n([\s\S]*?)```/g)].map((match) => match[1]).join("\n");
|
||||
const powershell = [...recovery.matchAll(/```powershell\n([\s\S]*?)```/g)].map((match) => match[1]).join("\n");
|
||||
const commands = [
|
||||
"git add --renormalize .",
|
||||
"git checkout-index --all --force --prefix=",
|
||||
@@ -211,35 +339,104 @@ for (const command of commands) {
|
||||
if (index < 0) throw new Error(`CRLF recovery lacks ordered command: ${command}`);
|
||||
prior = index;
|
||||
}
|
||||
for (let index = 0; index < lines.length; index += 1) {
|
||||
const command = lines[index].trim();
|
||||
if (command !== 'cp "$REPAIR_DIR/$path" "$path"' &&
|
||||
!command.startsWith("Copy-Item -LiteralPath") &&
|
||||
command !== "git reset --hard") continue;
|
||||
const warning = lines.slice(Math.max(0, index - 4), index).join(" ").toLowerCase();
|
||||
if (!warning.includes("warning") || !warning.includes("destructive") ||
|
||||
!warning.includes("backup") || !warning.includes("commit")) {
|
||||
throw new Error("worktree rewrite lacks an immediate destructive warning requiring backup/commit");
|
||||
}
|
||||
for (const token of [
|
||||
"set -euo pipefail", "validate_index_export", "validate_worktree_modes", "rewrite_index_entry", "git ls-files -s -z",
|
||||
"100644", "100755", "120000", "readlink", "ln -s", "if ! git checkout-index",
|
||||
]) {
|
||||
if (!shell.includes(token)) throw new Error(`POSIX CRLF repair lacks fail-closed semantic: ${token}`);
|
||||
}
|
||||
if (/\|\|\s*true|;\s*true\b/.test(shell)) throw new Error("POSIX CRLF repair contains a failure-bypass command");
|
||||
const exportAt = shell.indexOf("if ! git checkout-index");
|
||||
const validationAt = shell.indexOf("if ! validate_index_export", exportAt);
|
||||
const exportedBytesAt = shell.indexOf('if ! bash scripts/verify-line-endings.sh "$REPAIR_DIR"', validationAt);
|
||||
const rewriteAt = shell.indexOf("if ! git ls-files -s -z", exportedBytesAt);
|
||||
const finalModesAt = shell.indexOf("if ! validate_worktree_modes; then", rewriteAt);
|
||||
const finalAt = shell.indexOf("if ! bash scripts/verify-line-endings.sh; then", finalModesAt);
|
||||
if ([exportAt, validationAt, exportedBytesAt, rewriteAt, finalModesAt, finalAt].some((index) => index < 0) ||
|
||||
!(exportAt < validationAt && validationAt < exportedBytesAt && exportedBytesAt < rewriteAt && rewriteAt < finalModesAt && finalModesAt < finalAt)) {
|
||||
throw new Error("POSIX CRLF repair does not prove a complete export before destructive rewrite");
|
||||
}
|
||||
for (const token of [
|
||||
"$ErrorActionPreference = 'Stop'", "Assert-NativeSuccess 'index renormalization'",
|
||||
"Assert-NativeSuccess 'normalized index check'", "Assert-NativeSuccess 'index export'",
|
||||
"Assert-NativeSuccess 'index inventory'", "100644", "100755", "120000", "SymbolicLink",
|
||||
"-ErrorAction Stop", "$WorktreeItem", "Assert-NativeSuccess 'repaired worktree LF verification'",
|
||||
]) {
|
||||
if (!powershell.includes(token)) throw new Error(`PowerShell CRLF repair lacks failure propagation: ${token}`);
|
||||
}
|
||||
const warningPattern = /WARNING[^\n]*destructive[^\n]*(backup|commit)/i;
|
||||
const powerShellWarningAt = powershell.indexOf("# WARNING: destructive copy");
|
||||
const powerShellRewriteAt = powershell.indexOf("foreach ($Entry in $IndexEntries)", powerShellWarningAt);
|
||||
if (!warningPattern.test(shell.slice(Math.max(0, rewriteAt - 180), rewriteAt)) ||
|
||||
powerShellRewriteAt < 0 ||
|
||||
!warningPattern.test(powershell.slice(Math.max(0, powerShellRewriteAt - 180), powerShellRewriteAt))) {
|
||||
throw new Error("worktree rewrite lacks an immediate destructive warning requiring backup/commit");
|
||||
}
|
||||
NODE
|
||||
local fixture
|
||||
fixture="$(mktemp -d "${TMPDIR%/}/thoth-crlf-repair.XXXXXX")"
|
||||
trap 'rm -rf "$fixture"' RETURN
|
||||
git -C "$fixture" init -q
|
||||
printf '*.sh text eol=lf\n' >"$fixture/.gitattributes"
|
||||
printf '#!/bin/sh\r\nexit 0\r\n' >"$fixture/repair.sh"
|
||||
git -C "$fixture" add .gitattributes repair.sh 2>/dev/null
|
||||
git -C "$fixture" config --local core.autocrlf false
|
||||
git -C "$fixture" add --renormalize .
|
||||
local export_dir="$fixture-export"
|
||||
mkdir -p "$export_dir"
|
||||
git -C "$fixture" checkout-index --all --force --prefix="$export_dir/"
|
||||
"$root/scripts/verify-line-endings.sh" "$export_dir"
|
||||
cp "$export_dir/repair.sh" "$fixture/repair.sh"
|
||||
"$root/scripts/verify-line-endings.sh" "$fixture"
|
||||
rm -rf "$export_dir"
|
||||
echo "CRLF recovery rewrites worktree bytes passed"
|
||||
local repair_root repair_script real_git partial_repo clean_repo partial_output repair_status
|
||||
repair_root="$(mktemp -d "${TMPDIR%/}/thoth-crlf-repair.XXXXXX")"
|
||||
trap 'rm -rf "$repair_root"' RETURN
|
||||
repair_script="$repair_root/repair.sh"
|
||||
awk '
|
||||
/^## Recover an existing CRLF clone$/ { in_section=1; next }
|
||||
in_section && /^```sh$/ { in_code=1; next }
|
||||
in_code && /^```$/ { exit }
|
||||
in_code { print }
|
||||
' "$guide" >"$repair_script"
|
||||
chmod 0700 "$repair_script"
|
||||
|
||||
prepare_crlf_fixture() {
|
||||
local repository="$1"
|
||||
mkdir -p "$repository/scripts"
|
||||
git -C "$repository" init -q
|
||||
printf '*.sh text eol=lf\n' >"$repository/.gitattributes"
|
||||
printf '#!/bin/sh\nexit 0\n' >"$repository/repair.sh"
|
||||
printf 'target\n' >"$repository/target.txt"
|
||||
cp "$root/scripts/verify-line-endings.sh" "$repository/scripts/verify-line-endings.sh"
|
||||
ln -s target.txt "$repository/workspace-link"
|
||||
git -C "$repository" add .gitattributes repair.sh target.txt workspace-link \
|
||||
scripts/verify-line-endings.sh 2>/dev/null
|
||||
printf '#!/bin/sh\r\nexit 0\r\n' >"$repository/repair.sh"
|
||||
}
|
||||
|
||||
partial_repo="$repair_root/partial/worktree"
|
||||
mkdir -p "$partial_repo" "$repair_root/partial/bin"
|
||||
prepare_crlf_fixture "$partial_repo"
|
||||
real_git="$(command -v git)"
|
||||
printf '%s\n' \
|
||||
'#!/bin/sh' \
|
||||
'"$REAL_GIT" "$@"' \
|
||||
'status=$?' \
|
||||
'if [ $status -eq 0 ] && [ "$1" = checkout-index ]; then rm -f "$PARTIAL_EXPORT_PATH"; fi' \
|
||||
'exit $status' >"$repair_root/partial/bin/git"
|
||||
chmod 0700 "$repair_root/partial/bin/git"
|
||||
partial_output="$repair_root/partial/output"
|
||||
set +e
|
||||
(
|
||||
cd "$partial_repo"
|
||||
env PATH="$repair_root/partial/bin:$PATH" REAL_GIT="$real_git" \
|
||||
PARTIAL_EXPORT_PATH="$repair_root/partial/ThothII-lf-repair/repair.sh" \
|
||||
/bin/bash "$repair_script"
|
||||
) >"$partial_output" 2>&1
|
||||
repair_status=$?
|
||||
set -e
|
||||
[[ $repair_status -ne 0 ]] || { echo "partial CRLF export fixture was accepted" >&2; return 1; }
|
||||
LC_ALL=C grep -q $'\r' "$partial_repo/repair.sh" || {
|
||||
echo "partial CRLF export fixture rewrote bytes before complete validation" >&2; return 1;
|
||||
}
|
||||
[[ -L "$partial_repo/workspace-link" && "$(readlink "$partial_repo/workspace-link")" == target.txt ]] || {
|
||||
echo "partial CRLF export fixture changed the tracked symlink" >&2; return 1;
|
||||
}
|
||||
|
||||
clean_repo="$repair_root/clean/worktree"
|
||||
mkdir -p "$clean_repo"
|
||||
prepare_crlf_fixture "$clean_repo"
|
||||
(cd "$clean_repo" && /bin/bash "$repair_script") >/dev/null
|
||||
"$root/scripts/verify-line-endings.sh" "$clean_repo"
|
||||
[[ -L "$clean_repo/workspace-link" && "$(readlink "$clean_repo/workspace-link")" == target.txt ]] || {
|
||||
echo "successful CRLF repair did not preserve the mode-120000 symlink" >&2; return 1;
|
||||
}
|
||||
echo "CRLF recovery rewrites bytes and preserves mode-120000 symlinks passed"
|
||||
echo "Windows line-ending recovery guide contract passed"
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user