test(e2e): guard staging orphan cleanup coverage #1784

Merged
hongming merged 1 commits from fix/e2e-orphan-guard into main 2026-05-24 06:19:47 +00:00
+81 -4
View File
@@ -2,10 +2,16 @@
# lint_cleanup_traps.sh — regression gate for the OSS-shape program's
# "all E2E tests must have proper cleanup" bar (RFC #2873).
#
# Asserts: every shell file under tests/e2e/ that calls `mktemp` ALSO
# installs an `EXIT` trap somewhere in the file. The trap is the
# minimum-viable guarantee that scratch files won't leak when an
# assertion or curl exits the script non-zero.
# Asserts:
# 1. every shell file under tests/e2e/ that calls `mktemp` ALSO
# installs an `EXIT` trap somewhere in the file.
# 2. every staging tenant E2E script that provisions a real org uses a
# slug prefix caught by sweep-stale-e2e-orgs.yml and installs an
# EXIT trap.
#
# These are the minimum-viable guarantees that scratch files and real
# staging EC2 tenants converge back to zero when an assertion or curl
# exits the script non-zero.
#
# Why this lints (instead of the test runner enforcing): shell scripts
# can't easily be wrapped by an outer harness without breaking the
@@ -21,6 +27,7 @@
set -euo pipefail
cd "$(dirname "$0")"
repo_root="$(cd ../.. && pwd)"
violations=0
for f in test_*.sh; do
@@ -32,6 +39,76 @@ for f in test_*.sh; do
fi
done
if ! python3 - "$repo_root" <<'PY'
import re
import sys
from pathlib import Path
repo = Path(sys.argv[1])
e2e_dir = repo / "tests" / "e2e"
sweeper = repo / ".gitea" / "workflows" / "sweep-stale-e2e-orgs.yml"
errors: list[str] = []
sweeper_text = sweeper.read_text()
required_sweeper_prefixes = ('"e2e-"', '"rt-e2e-"')
for prefix in required_sweeper_prefixes:
if prefix not in sweeper_text:
errors.append(
f"::error file=.gitea/workflows/sweep-stale-e2e-orgs.yml::"
f"missing stale-org sweeper prefix {prefix}"
)
slug_assignment_re = re.compile(r'^\s*SLUG=(["\'])(?P<value>.+?)\1', re.MULTILINE)
covered_prefixes = ("e2e-", "rt-e2e-")
for path in sorted(e2e_dir.glob("test_*staging*.sh")):
text = path.read_text()
creates_org = "/cp/admin/orgs" in text and re.search(r"\bPOST\b", text)
deletes_org = "/cp/admin/tenants" in text and re.search(r"\bDELETE\b", text)
if not (creates_org or deletes_org):
continue
rel = path.relative_to(repo)
if not re.search(r"trap\s+.*\bEXIT\b", text):
errors.append(
f"::error file={rel}::staging tenant E2E touches CP org lifecycle "
"but has no EXIT trap for teardown"
)
assignments = [m.group("value") for m in slug_assignment_re.finditer(text)]
if not assignments:
errors.append(
f"::error file={rel}::staging tenant E2E touches CP org lifecycle "
"but has no quoted SLUG=... assignment for scoped cleanup"
)
continue
for value in assignments:
literal_prefix = re.split(r"[$`]", value, maxsplit=1)[0]
if not literal_prefix:
errors.append(
f"::error file={rel}::SLUG assignment starts with dynamic data "
f"({value!r}); use a fixed e2e-* or rt-e2e-* prefix so "
"sweep-stale-e2e-orgs can reap orphans"
)
continue
if not literal_prefix.startswith(covered_prefixes):
errors.append(
f"::error file={rel}::SLUG prefix {literal_prefix!r} is not "
"covered by sweep-stale-e2e-orgs.yml; use e2e-* or rt-e2e-*"
)
if errors:
print("\n".join(errors))
raise SystemExit(1)
print("✓ staging tenant E2E slug prefixes are covered by sweep-stale-e2e-orgs and use EXIT traps")
PY
then
violations=$((violations + 1))
fi
if [ "$violations" -gt 0 ]; then
echo "::error::$violations shell E2E file(s) leak scratch on early exit. See above."
exit 1