test(e2e): guard staging orphan cleanup coverage
ci-arm64-advisory / fast-checks (pull_request) Waiting to run
CI / Python Lint & Test (pull_request) Successful in 11s
Block internal-flavored paths / Block forbidden paths (pull_request) Successful in 12s
Lint shellcheck (arm64 pilot) / shellcheck-arm64 (pilot) (pull_request) Successful in 10s
E2E API Smoke Test / detect-changes (pull_request) Successful in 21s
CI / Detect changes (pull_request) Successful in 22s
E2E Chat / detect-changes (pull_request) Successful in 19s
Handlers Postgres Integration / detect-changes (pull_request) Successful in 12s
E2E Staging Canvas (Playwright) / detect-changes (pull_request) Successful in 17s
Lint no tenant GITEA or GITHUB token write / Scan for repo-host token write into tenant workspace surface (pull_request) Successful in 13s
Lint forbidden tenant-env keys / Scan workspace_secrets writers for forbidden env keys (pull_request) Successful in 13s
gate-check-v3 / gate-check (pull_request) Successful in 7s
Secret scan / Scan diff for credential-shaped strings (pull_request) Successful in 11s
sop-checklist / na-declarations (pull_request) N/A: (none)
sop-checklist / review-refire (pull_request) Has been skipped
sop-checklist / all-items-acked (pull_request) Successful in 13s
security-review / approved (pull_request) Failing after 13s
qa-review / approved (pull_request) Failing after 13s
sop-tier-check / tier-check (pull_request) Successful in 11s
CI / Platform (Go) (pull_request) Successful in 6s
CI / Canvas (Next.js) (pull_request) Successful in 4s
E2E Chat / E2E Chat (pull_request) Successful in 4s
CI / Shellcheck (E2E scripts) (pull_request) Successful in 16s
lint-required-no-paths / lint-required-no-paths (pull_request) Successful in 1m4s
Handlers Postgres Integration / Handlers Postgres Integration (pull_request) Successful in 9s
CI / Canvas Deploy Reminder (pull_request) Has been skipped
E2E Staging Canvas (Playwright) / Canvas tabs E2E (pull_request) Successful in 3s
CI / all-required (pull_request) Successful in 1m40s
E2E API Smoke Test / E2E API Smoke Test (pull_request) Successful in 1m8s
audit-force-merge / audit (pull_request) Successful in 7s

This commit is contained in:
2026-05-23 23:16:38 -07:00
parent 2e027df890
commit f1571a04ab
+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