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
|
- name: Test declarative deployment bundle
|
||||||
working-directory: govoplan
|
working-directory: govoplan
|
||||||
run: python -m unittest -v tests.test_deployment_installer
|
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
|
- name: Build single-file deployer artifact
|
||||||
working-directory: govoplan
|
working-directory: govoplan
|
||||||
run: |
|
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
|
spreadsheets, overly deep/long archive paths and oversized padding intermediates
|
||||||
can now fail early with diagnostics. No stored documents or configurations were
|
can now fail early with diagnostics. No stored documents or configurations were
|
||||||
deleted or silently migrated.
|
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" tools/checks/check-webui-package-facades.py
|
||||||
"$PYTHON" -m unittest tests.test_webui_package_facades
|
"$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_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_evidence
|
||||||
"$PYTHON" -m unittest tests.test_capability_fit_generation tests.test_capability_fit_review
|
"$PYTHON" -m unittest tests.test_capability_fit_generation tests.test_capability_fit_review
|
||||||
"$PYTHON" tools/assessments/generate-capability-fit-report.py --check
|
"$PYTHON" tools/assessments/generate-capability-fit-report.py --check
|
||||||
|
|||||||
@@ -19,8 +19,9 @@ retry() {
|
|||||||
for attempt in 1 2 3; do
|
for attempt in 1 2 3; do
|
||||||
if "$@"; then
|
if "$@"; then
|
||||||
return 0
|
return 0
|
||||||
fi
|
else
|
||||||
status=$?
|
status=$?
|
||||||
|
fi
|
||||||
if [[ "$attempt" == 3 ]]; then
|
if [[ "$attempt" == 3 ]]; then
|
||||||
return "$status"
|
return "$status"
|
||||||
fi
|
fi
|
||||||
|
|||||||
Reference in New Issue
Block a user