fix: isolate CI Keychain access from the production app
Build / macos (push) Successful in 27s

This commit is contained in:
2026-09-10 00:33:30 +02:00
parent 31584792e6
commit 7584863392
3 changed files with 30 additions and 35 deletions
+26 -32
View File
@@ -1,8 +1,7 @@
#!/usr/bin/env python3
"""Check the CI app's unauthenticated API without changing saved preferences."""
import json
import os
import signal
import plistlib
import socket
import subprocess
import sys
@@ -19,23 +18,27 @@ with tempfile.TemporaryDirectory(prefix="smoke-", dir=cache) as directory:
staged = Path(directory) / "Meetingnotes.app"
subprocess.run(["ditto", sys.argv[1], str(staged)], check=True)
app = staged / "Contents/MacOS/Meetingnotes"
with (staged / "Contents/Info.plist").open("rb") as info_file:
bundle_id = plistlib.load(info_file)["CFBundleIdentifier"]
assert bundle_id == "net.jamesbone.meetingnotes.ci", "Smoke tests require the isolated CI app"
def clear_test_token():
result = subprocess.run(
["security", "delete-generic-password", "-s", bundle_id, "-a", "muteDeckAPIToken"],
capture_output=True, timeout=10,
)
if result.returncode not in (0, 44): # 44: item does not exist
raise RuntimeError("Could not clear the CI Keychain token")
clear_test_token()
with socket.socket() as listener:
listener.bind(("127.0.0.1", 0))
port = listener.getsockname()[1]
with tempfile.NamedTemporaryFile() as log:
with tempfile.TemporaryFile() as log:
process = subprocess.Popen(
["open", "-n", "-W", "-g", "-a", str(staged), "--stdout", log.name, "--stderr", log.name, "--args", "-ApplePersistenceIgnoreState", "YES", "-muteDeckAPIEnabled", "YES", "-muteDeckAPIPort", str(port), "-hasCompletedOnboarding", "YES", "-hasAcceptedTerms", "YES", "-SUEnableAutomaticChecks", "NO"],
[str(app), "-muteDeckAPIEnabled", "YES", "-muteDeckAPIPort", str(port), "-hasCompletedOnboarding", "YES", "-hasAcceptedTerms", "YES", "-SUEnableAutomaticChecks", "NO"],
stdout=log, stderr=subprocess.STDOUT,
)
def app_pid():
# The unique staged path identifies only this test's app instance.
listing = subprocess.check_output(["ps", "-axo", "pid=,command="], text=True)
for line in listing.splitlines():
fields = line.strip().split(None, 1)
if len(fields) == 2 and fields[1].startswith(str(app) + " "):
return int(fields[0])
return None
try:
for attempt in range(30):
if process.poll() is not None:
@@ -57,27 +60,18 @@ with tempfile.TemporaryDirectory(prefix="smoke-", dir=cache) as directory:
raise RuntimeError("Recording status allowed an unauthenticated request")
print("Local API readiness and authentication checks passed")
except Exception:
pid = app_pid()
if pid is not None:
sample = subprocess.run(["sample", str(pid), "1", "1"], capture_output=True, text=True, timeout=10)
print(sample.stdout[:14000], file=sys.stderr)
if process.poll() is None:
sample = subprocess.run(["sample", str(process.pid), "1", "1"], capture_output=True, text=True, timeout=10)
print(sample.stdout.split("Binary Images:")[0], file=sys.stderr)
log.seek(0)
print(log.read().decode(errors="replace")[-8000:], file=sys.stderr)
raise
finally:
pid = app_pid()
if pid is not None:
try:
os.kill(pid, signal.SIGTERM)
except ProcessLookupError:
pass
try:
process.wait(timeout=10)
except subprocess.TimeoutExpired:
if app_pid() == pid and pid is not None:
try:
os.kill(pid, signal.SIGKILL)
except ProcessLookupError:
pass
if process.poll() is None:
process.terminate()
process.wait(timeout=10)
try:
process.wait(timeout=10)
except subprocess.TimeoutExpired:
process.kill()
process.wait()
clear_test_token()