fix(release): preserve failed installer retry status
Cover all retry call sites with isolated regressions in focused checks and installer CI. Add EN/DE operating guidance and record the unreleased Xrechnung and installer audit follow-ups without changing immutable release artifacts. Refs #54
This commit is contained in:
@@ -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: |
|
||||
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
|
||||
@@ -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()
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user