diff --git a/.gitea/workflows/deployment-installer.yml b/.gitea/workflows/deployment-installer.yml index ee781fa..a955837 100644 --- a/.gitea/workflows/deployment-installer.yml +++ b/.gitea/workflows/deployment-installer.yml @@ -23,6 +23,9 @@ jobs: - name: Test declarative deployment bundle working-directory: govoplan run: python -m unittest -v tests.test_deployment_installer + - name: Test WebUI installer retry failures + working-directory: govoplan + run: python -m unittest -v tests.test_webui_release_dependency_retries - name: Build single-file deployer artifact working-directory: govoplan run: | diff --git a/docs/operations/WEBUI_RELEASE_DEPENDENCY_RETRIES.md b/docs/operations/WEBUI_RELEASE_DEPENDENCY_RETRIES.md new file mode 100644 index 0000000..ac9ce58 --- /dev/null +++ b/docs/operations/WEBUI_RELEASE_DEPENDENCY_RETRIES.md @@ -0,0 +1,91 @@ +# WebUI release dependency installer retries + +## English + +This operational note covers +[`install-webui-release-dependencies.sh`](../../tools/release/install-webui-release-dependencies.sh) +and the exit-status repair tracked in +[Meta #54](https://git.add-ideas.de/GovOPlaN/govoplan/issues/54). +It applies to release administrators using the legacy runtime WebUI installer; +there are no new application settings, permissions, or end-user workflows. + +Each retried npm install or Git clone has at most three attempts. The installer +waits 10 seconds after the first failure and 20 seconds after the second, and +continues immediately after success. If all attempts fail, it exits with the +last command's nonzero status. Its `set -e` execution stops before subsequent +installation stages; callers using `set -e` also stop before subsequent work. +Previously, the retry helper could report success after three failures because +it captured the status of a completed `if` statement instead of the command. + +On exhaustion, inspect the npm or Git error and correct the reported cause +before rerunning the installation. The temporary dependency workspace is +removed on exit. Earlier changes to `package.json`, removal of `package-lock.json`, +cache cleaning, and completed dependency installations are not rolled back; +prepare a fresh disposable release workspace when a clean retry is required. + +The repair preserves the existing retry count, backoff, cache behavior, and +peer-resolution flags. It does not lift the runtime publication hold tracked in +[Meta #52](https://git.add-ideas.de/GovOPlaN/govoplan/issues/52). +Review the historical `--legacy-peer-deps` workaround separately before lifting +that hold. Strict disposable Git-release and signed catalog verification do not +use this installer; strict release verification must not bypass peer checks. +See [Package Registry Releases](PACKAGE_REGISTRY_RELEASES.md) for release context. + +Run the isolated regression suite from the meta repository: + +```sh +python3 -m unittest -v tests.test_webui_release_dependency_retries +``` + +The suite executes the actual Bash installer and a caller using `set -e`, with +local npm, Git, Node, and sleep stubs. It covers success on attempts one, two, and +three, final failure status, backoff, and termination at each retry call site. +It performs no network access, real waiting, or changes to the real npm cache. +It checks shell control flow, not package resolution or runtime publication. + +## Deutsch + +Dieser Betriebshinweis beschreibt +[`install-webui-release-dependencies.sh`](../../tools/release/install-webui-release-dependencies.sh) +und die unter [Meta #54](https://git.add-ideas.de/GovOPlaN/govoplan/issues/54) +erfasste Korrektur des Rückgabestatus. Er richtet sich an Release-Administratoren, +die den bisherigen WebUI-Installer für Laufzeit-Releases verwenden. Neue +Anwendungseinstellungen, Berechtigungen oder Endanwenderabläufe entstehen nicht. + +Jede wiederholte npm-Installation und jeder Git-Klon erhält höchstens drei +Versuche. Nach dem ersten Fehlschlag wartet der Installer 10 Sekunden, nach dem +zweiten 20 Sekunden; nach einem Erfolg fährt er sofort fort. Scheitern alle +Versuche, endet er mit dem letzten von null verschiedenen Rückgabestatus. +Durch `set -e` werden nachfolgende Installationsschritte nicht ausgeführt; +auch aufrufende Skripte mit `set -e` brechen vor ihren nächsten Schritten ab. +Bisher konnte die Hilfsfunktion nach drei Fehlschlägen Erfolg melden, weil sie +den Status der abgeschlossenen `if`-Anweisung statt des Befehls übernahm. + +Prüfen Sie nach dem Abbruch die npm- oder Git-Fehlermeldung und beheben Sie deren +Ursache vor einem erneuten Installationslauf. Das temporäre Verzeichnis für +Abhängigkeiten wird beim Beenden entfernt. Vorherige Änderungen an `package.json`, +das Entfernen von `package-lock.json`, die Cache-Bereinigung und abgeschlossene +Installationen werden nicht zurückgerollt. Bereiten Sie bei Bedarf einen neuen +temporären Release-Arbeitsbereich für einen sauberen Wiederholungslauf vor. + +Die Korrektur erhält Anzahl und Wartezeiten der Versuche, Cache-Verhalten und +Optionen zur Peer-Auflösung. Die Sperre für Laufzeitveröffentlichungen aus +[Meta #52](https://git.add-ideas.de/GovOPlaN/govoplan/issues/52) bleibt bestehen. +Der bisherige Einsatz von `--legacy-peer-deps` muss vor ihrer Aufhebung gesondert +geprüft werden. Die strenge Git-Release-Prüfung in einem temporären Arbeitsbereich +und die Prüfung signierter Kataloge verwenden diesen Installer nicht; die strenge +Release-Prüfung darf Peer-Prüfungen nicht umgehen. Weitere Zusammenhänge erläutert +[Package Registry Releases](PACKAGE_REGISTRY_RELEASES.md). + +Führen Sie die isolierten Regressionstests im Meta-Repository aus: + +```sh +python3 -m unittest -v tests.test_webui_release_dependency_retries +``` + +Die Tests führen den tatsächlichen Bash-Installer und ein aufrufendes Skript mit +`set -e` aus. Lokale Testprogramme ersetzen npm, Git, Node und sleep. Geprüft werden +Erfolge im ersten, zweiten und dritten Versuch, der letzte Fehlerstatus, +Warteintervalle und der Abbruch an jeder Aufrufstelle. Es gibt keine +Netzwerkzugriffe, echten Wartezeiten oder Änderungen am tatsächlichen npm-Cache. +Die Tests prüfen den Shell-Ablauf, nicht die Paketauflösung oder Veröffentlichung. diff --git a/docs/security/SECURITY_PERFORMANCE_REVIEW_2026-09-08.md b/docs/security/SECURITY_PERFORMANCE_REVIEW_2026-09-08.md index 83cfa39..bec5fc3 100644 --- a/docs/security/SECURITY_PERFORMANCE_REVIEW_2026-09-08.md +++ b/docs/security/SECURITY_PERFORMANCE_REVIEW_2026-09-08.md @@ -174,3 +174,28 @@ permissions must be corrected rather than weakening the guard. Extreme sparse spreadsheets, overly deep/long archive paths and oversized padding intermediates can now fail early with diagnostics. No stored documents or configurations were deleted or silently migrated. + +## Post-release follow-up — 2026-09-08 + +The findings and scanner counts above describe the original audit snapshot. +The following source fixes are subsequent to the frozen `0.1.45` composition; +they do not change its immutable tags or published package bytes. + +- [Xrechnung #2](https://git.add-ideas.de/GovOPlaN/govoplan-xrechnung/issues/2) + now enforces the existing shared 2 MiB stdout/stderr limit during execution + and kills/reaps the direct validator on overflow, timeout or cancellation. + Report reads are bounded to 16 MiB plus one probe byte before interpretation. + The 30-test module suite passes; noisy-child and report-read regressions were + also demonstrated to fail against the previous source. Owning EN/DE static + documentation is updated. POSIX pipe capture is required; disk quotas, + descendant isolation and process-level CPU/memory limits remain separate work. +- [Meta #54](https://git.add-ideas.de/GovOPlaN/govoplan/issues/54) now preserves + the last command's failure status after exhausted installer retries. Twelve + isolated stage/scenario combinations cover every retry call site, success, + backoff and caller termination under `set -e`. The test is included in the + focused checks and installer CI. See the bilingual + [installer retry note](../operations/WEBUI_RELEASE_DEPENDENCY_RETRIES.md). + +These are unreleased follow-up source changes, not a new runtime release or +deployment. The runtime-image hold under Meta #52 remains in force; the +historical peer-dependency workaround still needs its separate review. diff --git a/tests/test_webui_release_dependency_retries.py b/tests/test_webui_release_dependency_retries.py new file mode 100644 index 0000000..bc95f85 --- /dev/null +++ b/tests/test_webui_release_dependency_retries.py @@ -0,0 +1,154 @@ +from __future__ import annotations + +import os +from pathlib import Path +import subprocess +import tempfile +import textwrap +import unittest + + +META_ROOT = Path(__file__).resolve().parents[1] +INSTALLER = META_ROOT / "tools" / "release" / "install-webui-release-dependencies.sh" +STAGES = ("base", "clone", "modules") + + +class WebUIReleaseDependencyRetryTests(unittest.TestCase): + def _run_installer( + self, stage: str, statuses: tuple[int, ...] + ) -> tuple[subprocess.CompletedProcess[str], list[str]]: + with tempfile.TemporaryDirectory(prefix="govoplan-installer-retry-test-") as directory: + root = Path(directory) + stub_bin = root / "bin" + stub_bin.mkdir() + core_root = root / "core" + webui = core_root / "web ui" + webui.mkdir(parents=True) + work_root = root / "work" + work_root.mkdir() + log = root / "commands.log" + stub = "#!/usr/bin/env bash\nset -euo pipefail\n" + textwrap.dedent( + r""" + case "${0##*/}" in + node) + # Supply the shell's dependency list without requiring Node. + printf '%s\t%s\n' '@govoplan/example-webui' \ + 'git+https://example.invalid/module.git#v1.0.0' > "$GOVOPLAN_DEPS" + printf 'node\n' >> "$RETRY_TEST_LOG" + exit 0 + ;; + sleep) + printf 'sleep %s\n' "$*" >> "$RETRY_TEST_LOG" + exit 0 + ;; + npm) + case "${1:-}" in + cache) + printf 'cache\n' >> "$RETRY_TEST_LOG" + exit 0 + ;; + install) + stage=base + for argument in "$@"; do + if [[ "$argument" == --no-save ]]; then + stage=modules + fi + done + ;; + *) exit 98 ;; + esac + ;; + git) + [[ "${1:-}" == clone ]] || exit 98 + stage=clone + ;; + *) exit 98 ;; + esac + + status=0 + if [[ "$stage" == "$RETRY_TEST_STAGE" ]]; then + attempt=0 + counter="$RETRY_TEST_ROOT/$stage.count" + if [[ -f "$counter" ]]; then + read -r attempt < "$counter" + fi + read -r -a statuses <<< "$RETRY_TEST_STATUSES" + status="${statuses[$attempt]:-99}" + printf '%s\n' "$((attempt + 1))" > "$counter" + fi + printf '%s %s\n' "$stage" "$status" >> "$RETRY_TEST_LOG" + exit "$status" + """ + ) + for name in ("node", "npm", "git", "sleep"): + executable = stub_bin / name + executable.write_text(stub, encoding="utf-8") + executable.chmod(0o755) + + env = os.environ.copy() + env.update( + { + "PATH": f"{stub_bin}:{os.defpath}", + "TMPDIR": str(work_root), + "GOVOPLAN_CORE_ROOT": str(core_root), + "GOVOPLAN_WEBUI_PACKAGE_LOCK": "", + "GOVOPLAN_WEBUI_PACKAGE_DIR": "", + "RETRY_TEST_ROOT": str(root), + "RETRY_TEST_LOG": str(log), + "RETRY_TEST_STAGE": stage, + "RETRY_TEST_STATUSES": " ".join(map(str, statuses)), + } + ) + result = subprocess.run( + [ + "bash", + "-c", + 'set -euo pipefail; bash "$1" "$2"; ' + 'printf "caller-continued\\n" >> "$RETRY_TEST_LOG"', + "retry-test-caller", + str(INSTALLER), + str(webui), + ], + cwd=root, + env=env, + text=True, + capture_output=True, + timeout=10, + check=False, + ) + self.assertEqual(list(work_root.iterdir()), [], result.stderr) + return result, log.read_text(encoding="utf-8").splitlines() + + def _assert_attempts(self, statuses: tuple[int, ...]) -> None: + for retried_stage in STAGES: + with self.subTest(stage=retried_stage, statuses=statuses): + result, commands = self._run_installer(retried_stage, statuses) + expected = ["node", "cache"] + for stage in STAGES: + attempts = statuses if stage == retried_stage else (0,) + for index, status in enumerate(attempts): + expected.append(f"{stage} {status}") + if status and index < 2: + expected.append(f"sleep {(index + 1) * 10}") + if attempts[-1]: + break + if statuses[-1] == 0: + expected.append("caller-continued") + self.assertEqual(result.returncode, statuses[-1], result.stderr) + self.assertEqual(commands, expected, result.stderr) + + def test_success_on_first_attempt(self) -> None: + self._assert_attempts((0,)) + + def test_success_on_second_attempt(self) -> None: + self._assert_attempts((17, 0)) + + def test_success_on_third_attempt(self) -> None: + self._assert_attempts((17, 23, 0)) + + def test_exhaustion_preserves_final_status_and_stops_callers(self) -> None: + self._assert_attempts((17, 23, 47)) + + +if __name__ == "__main__": + unittest.main() diff --git a/tools/checks/check-focused.sh b/tools/checks/check-focused.sh index ea75ff0..91dcc67 100644 --- a/tools/checks/check-focused.sh +++ b/tools/checks/check-focused.sh @@ -50,7 +50,7 @@ cd "$META_ROOT" "$PYTHON" tools/checks/check-webui-package-facades.py "$PYTHON" -m unittest tests.test_webui_package_facades "$PYTHON" -m unittest tests.test_module_package_workflows tests.test_package_registry_release -"$PYTHON" -m unittest tests.test_deployment_installer +"$PYTHON" -m unittest tests.test_deployment_installer tests.test_webui_release_dependency_retries "$PYTHON" -m unittest tests.test_capability_fit_evidence "$PYTHON" -m unittest tests.test_capability_fit_generation tests.test_capability_fit_review "$PYTHON" tools/assessments/generate-capability-fit-report.py --check diff --git a/tools/release/install-webui-release-dependencies.sh b/tools/release/install-webui-release-dependencies.sh index eb3052c..c55acf4 100644 --- a/tools/release/install-webui-release-dependencies.sh +++ b/tools/release/install-webui-release-dependencies.sh @@ -19,8 +19,9 @@ retry() { for attempt in 1 2 3; do if "$@"; then return 0 + else + status=$? fi - status=$? if [[ "$attempt" == 3 ]]; then return "$status" fi