fix(release): refuse to gate or record against a foreign acceptance server
Quality gates / quality (push) Successful in 16m30s
Quality gates / quality (push) Successful in 16m30s
A leftover checkout answering on 127.0.0.1:4173 satisfied the readiness probe, so every acceptance flow and the recorded demo described another tree while the build reported green. The builder now fails closed if the port already has a listener, re-checks the spawned server after readiness, and verifies the served app.js and src/domain.js bytes match the clone under test before the first gate. Verified against a real stale tree: the occupied-port guard aborts, the foreign-bytes guard aborts, and the matching tree still passes.
This commit is contained in:
1 parent
4ac71915fb
commit
3565ce87a4
2 files changed
+47
No files matched your search
@@ -9,6 +9,7 @@ import os
|
||||
from pathlib import Path
|
||||
import re
|
||||
import shutil
|
||||
import socket
|
||||
import subprocess
|
||||
import sys
|
||||
import tarfile
|
||||
@@ -76,6 +77,29 @@ def sha256(path: Path) -> str:
|
||||
return digest.hexdigest()
|
||||
|
||||
|
||||
def assert_port_free(host: str, port: int) -> None:
|
||||
"""Fail closed when something is already listening where the acceptance server must bind."""
|
||||
with socket.socket() as sock:
|
||||
sock.settimeout(1.0)
|
||||
if sock.connect_ex((host, port)) == 0:
|
||||
raise SystemExit(
|
||||
f"Port {port} is already served by another process; refusing to gate or record this "
|
||||
"release against a tree that is not the one under test."
|
||||
)
|
||||
|
||||
|
||||
def assert_served_tree(tree: Path, host: str, port: int) -> None:
|
||||
"""Prove the responding server is the clone under test, not a stale checkout on the same port."""
|
||||
for relative in ("src/domain.js", "app.js"):
|
||||
served = urllib.request.urlopen(f"http://{host}:{port}/{relative}", timeout=5).read()
|
||||
expected = (tree / relative).read_bytes()
|
||||
if hashlib.sha256(served).hexdigest() != hashlib.sha256(expected).hexdigest():
|
||||
raise SystemExit(
|
||||
f"The acceptance server is serving {relative} from a different tree; "
|
||||
"every gate and the demo would describe a release that is not this commit."
|
||||
)
|
||||
|
||||
|
||||
def tracked_files(tree: Path) -> list[str]:
|
||||
names = run(["git", "ls-files"], tree, capture=True).splitlines()
|
||||
return sorted(name for name in names if name and not name.startswith(EXCLUDED_PREFIXES)
|
||||
@@ -133,6 +157,10 @@ def main() -> int:
|
||||
run(["python3", "scripts/build_vision_corpus.py", "--verify"], tree)
|
||||
server_env = dict(os.environ)
|
||||
server_env.update({"HOST": "127.0.0.1", "PORT": "4173", "TIMMY_VISION_ENABLED": "false"})
|
||||
# A leftover server from another checkout answers on this port and the readiness probe cannot
|
||||
# tell it apart from ours, so every gate and the recorded demo would silently describe a
|
||||
# different tree. Refuse to build over a live listener, and verify the served bytes afterwards.
|
||||
assert_port_free("127.0.0.1", 4173)
|
||||
server_log = (release_dir / "acceptance-server.log").open("w", encoding="utf-8")
|
||||
server = subprocess.Popen(["node", "server.mjs"], cwd=tree, env=server_env,
|
||||
stdout=server_log, stderr=subprocess.STDOUT, text=True)
|
||||
@@ -147,6 +175,9 @@ def main() -> int:
|
||||
time.sleep(0.1)
|
||||
else:
|
||||
raise SystemExit("Acceptance server did not become ready")
|
||||
if server.poll() is not None:
|
||||
raise SystemExit("Acceptance server exited after reporting ready")
|
||||
assert_served_tree(tree, "127.0.0.1", 4173)
|
||||
run(["npm", "run", "test:ui"], tree)
|
||||
run(["npm", "run", "test:photo"], tree)
|
||||
run(["npm", "run", "test:mobile-capture"], tree)
|
||||
|
||||
@@ -373,3 +373,19 @@ test('release demo shows a versioned backup, an upgraded older file, and a refus
|
||||
assert.match(builder, /"portable_backup_restore": "passed"/);
|
||||
assert.match(builder, /"test:portability"/);
|
||||
});
|
||||
|
||||
test('the builder refuses to gate or record a release against a foreign server', async () => {
|
||||
const builder = await readFile(builderPath, 'utf8');
|
||||
|
||||
// A leftover checkout answering on the acceptance port once satisfied every flow and the demo,
|
||||
// so the release described a tree that was not the commit under test. The builder must refuse a
|
||||
// live listener before it starts, then prove the served bytes are this tree's before any gate.
|
||||
assert.match(builder, /def assert_port_free/);
|
||||
assert.match(builder, /assert_port_free\("127\.0\.0\.1", 4173\)/);
|
||||
assert.match(builder, /def assert_served_tree/);
|
||||
assert.match(builder, /assert_served_tree\(tree, "127\.0\.0\.1", 4173\)/);
|
||||
const identityCheck = builder.indexOf('assert_served_tree(tree, "127.0.0.1", 4173)');
|
||||
const firstFlow = builder.indexOf('run(["npm", "run", "test:ui"]');
|
||||
assert.ok(identityCheck > 0, 'the served-tree check must be present');
|
||||
assert.ok(firstFlow > identityCheck, 'the served-tree check must run before the first acceptance flow');
|
||||
});
|
||||
Reference in new issue
Block a user