fix: launch CI smoke app through macOS Launch Services
Build / macos (push) Failing after 1m3s

This commit is contained in:
2026-09-10 00:25:09 +02:00
parent 58fbe56298
commit 31584792e6
2 changed files with 33 additions and 11 deletions
+2 -1
View File
@@ -88,7 +88,8 @@ identity keeps the same sandbox container across updates.
`.gitea/workflows/build.yml` builds universal macOS artifacts for pushes and pull `.gitea/workflows/build.yml` builds universal macOS artifacts for pushes and pull
requests to `main`. The repository-scoped `mac-mini-meetingnotes` runner uses the requests to `main`. The repository-scoped `mac-mini-meetingnotes` runner uses the
`macos-arm64` label. Smoke tests use a separate CI bundle identifier and temporary `macos-arm64` label. Smoke tests use a separate CI bundle identifier and temporary
launch preferences. Release jobs import the original Developer ID certificate launch preferences, an internal-volume staging directory, and Launch Services
so the GUI app runs in the logged-in Mac session. Release jobs import the original Developer ID certificate
into a temporary keychain and keep the original Sparkle signing key in Gitea into a temporary keychain and keep the original Sparkle signing key in Gitea
Actions secrets. They never publish from a development branch. Actions secrets. They never publish from a development branch.
+31 -10
View File
@@ -1,6 +1,8 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
"""Check the CI app's unauthenticated API without changing saved preferences.""" """Check the CI app's unauthenticated API without changing saved preferences."""
import json import json
import os
import signal
import socket import socket
import subprocess import subprocess
import sys import sys
@@ -20,11 +22,20 @@ with tempfile.TemporaryDirectory(prefix="smoke-", dir=cache) as directory:
with socket.socket() as listener: with socket.socket() as listener:
listener.bind(("127.0.0.1", 0)) listener.bind(("127.0.0.1", 0))
port = listener.getsockname()[1] port = listener.getsockname()[1]
with tempfile.TemporaryFile() as log: with tempfile.NamedTemporaryFile() as log:
process = subprocess.Popen( process = subprocess.Popen(
[str(app), "-muteDeckAPIEnabled", "YES", "-muteDeckAPIPort", str(port), "-hasCompletedOnboarding", "YES", "-hasAcceptedTerms", "YES", "-SUEnableAutomaticChecks", "NO"], ["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"],
stdout=log, stderr=subprocess.STDOUT, 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: try:
for attempt in range(30): for attempt in range(30):
if process.poll() is not None: if process.poll() is not None:
@@ -46,17 +57,27 @@ with tempfile.TemporaryDirectory(prefix="smoke-", dir=cache) as directory:
raise RuntimeError("Recording status allowed an unauthenticated request") raise RuntimeError("Recording status allowed an unauthenticated request")
print("Local API readiness and authentication checks passed") print("Local API readiness and authentication checks passed")
except Exception: except Exception:
if process.poll() is None: pid = app_pid()
sample = subprocess.run(["sample", str(process.pid), "1", "1"], capture_output=True, text=True, timeout=10) 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) print(sample.stdout[:14000], file=sys.stderr)
log.seek(0) log.seek(0)
print(log.read().decode(errors="replace")[-8000:], file=sys.stderr) print(log.read().decode(errors="replace")[-8000:], file=sys.stderr)
raise raise
finally: finally:
if process.poll() is None: pid = app_pid()
process.terminate() if pid is not None:
try: try:
process.wait(timeout=10) os.kill(pid, signal.SIGTERM)
except subprocess.TimeoutExpired: except ProcessLookupError:
process.kill() pass
process.wait() 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
process.terminate()
process.wait(timeout=10)