From cf277064cc90d7a24ab07c596d03cb6681825f94 Mon Sep 17 00:00:00 2001 From: Thomas SharedInbox Date: Fri, 15 May 2026 19:08:55 +0200 Subject: [PATCH] feat(builds): populate builds page with Linux and Android history (#94) The builds page at /builds/ was empty because generate-build-history only ran inside deploy-playstore; if that job failed early (e.g. Play Store secrets not configured) the website was never updated, and the build-linux job never triggered a website update at all. Changes: - generate_build_history.py: extend to cover Linux tarballs in addition to Android APKs, capped at MAX_BUILDS_PER_PLATFORM (30) each - Taskfile: add website-publish task (generate-build-history + website-deploy), exclude *.tar.gz from rsync, update descriptions - .forgejo/workflows/ci.yml: add publish-website job that waits for both build-linux and deploy-playstore (using always() so it runs even when deploy-playstore fails), then removes the duplicate generate/deploy steps from deploy-playstore - .github/workflows/ci.yml: add deploy job that deploys Linux build, generates build history, builds Hugo site, and rsyncs to server - .gitignore: ignore website/content/builds/_index.md (generated), Python __pycache__, and widget test failure screenshots - stalwart-dev/integration_ui_test.sh: use ${USER:-$(id -un)} for robustness in environments where USER is unset - scripts/test_generate_build_history.py: unit tests for parse_builds and render_entries covering both platforms Generated content (builds/_index.md and per-day pages) is not tracked in git; it is produced at CI time and rsynced to the server. Co-Authored-By: Claude Sonnet 4.6 --- .forgejo/workflows/ci.yml | 37 +++++-- .github/workflows/ci.yml | 96 ++++++++++++++++ .gitignore | 6 + Taskfile.yml | 14 ++- scripts/generate_build_history.py | 146 +++++++++++++++++-------- scripts/test_generate_build_history.py | 113 +++++++++++++++++++ stalwart-dev/integration_ui_test.sh | 8 +- 7 files changed, 360 insertions(+), 60 deletions(-) create mode 100644 scripts/test_generate_build_history.py diff --git a/.forgejo/workflows/ci.yml b/.forgejo/workflows/ci.yml index 065e034..d0a94ad 100644 --- a/.forgejo/workflows/ci.yml +++ b/.forgejo/workflows/ci.yml @@ -127,16 +127,35 @@ jobs: ANDROID_KEYSTORE_PASSWORD: ${{ secrets.ANDROID_KEYSTORE_PASSWORD }} run: nix develop --no-warn-dirty --command task deploy-apk-to-server - - name: Generate build history - continue-on-error: true - env: - SSH_USER: ${{ secrets.SSH_USER }} - SSH_HOST: ${{ secrets.SSH_HOST }} - run: nix develop --no-warn-dirty --command task generate-build-history + publish-website: + name: Publish Website Build History + runs-on: self-hosted + needs: [build-linux, deploy-playstore] + if: | + always() && + github.ref == 'refs/heads/main' && + (needs.build-linux.result == 'success' || needs.deploy-playstore.result == 'success') - - name: Deploy website - continue-on-error: true + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 1 + + - name: Enable Nix flakes + run: | + mkdir -p ~/.config/nix + echo "experimental-features = nix-command flakes" >> ~/.config/nix/nix.conf + + - name: Set up SSH key + env: + SSH_PRIVATE_KEY: ${{ secrets.SSH_PRIVATE_KEY }} + run: | + mkdir -p ~/.ssh + echo "$SSH_PRIVATE_KEY" > ~/.ssh/id_rsa + chmod 600 ~/.ssh/id_rsa + + - name: Generate build history and deploy website env: SSH_USER: ${{ secrets.SSH_USER }} SSH_HOST: ${{ secrets.SSH_HOST }} - run: nix develop --no-warn-dirty --command task website-deploy + run: nix develop --no-warn-dirty --command task website-publish diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 8381efa..8991764 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -151,3 +151,99 @@ jobs: - name: Build Linux release run: flutter build linux --release + + deploy: + name: Deploy Linux build & publish website + runs-on: ubuntu-latest + needs: build-linux + if: github.event_name == 'push' && github.ref == 'refs/heads/main' + env: + SSH_HOST: ${{ secrets.SSH_HOST }} + SSH_USER: ${{ secrets.SSH_USER }} + + steps: + - uses: actions/checkout@v4 + + - name: Install build & deploy dependencies + run: | + sudo apt-get update -q + sudo apt-get install -y --no-install-recommends \ + libgtk-3-dev pkg-config cmake ninja-build clang \ + libsecret-1-dev hugo rsync + + - uses: subosito/flutter-action@v2 + with: + flutter-version: "3.41.6" + channel: stable + cache: true + + - name: Cache pub packages + uses: actions/cache@v4 + with: + path: ~/.pub-cache + key: pub-${{ hashFiles('pubspec.lock') }} + restore-keys: pub- + + - name: Install dependencies + run: flutter pub get + + - name: Generate Drift code + run: flutter pub run build_runner build --delete-conflicting-outputs + + - name: Generate changelog + run: | + mkdir -p assets + git log -n 50 \ + --pretty=format:'* %ad [%h](https://codeberg.org/guettli/sharedinbox/commit/%H): %s' \ + --date=short > assets/changelog.txt + + - name: Setup SSH + run: | + mkdir -p ~/.ssh + printf '%s\n' "${{ secrets.SSH_PRIVATE_KEY }}" > ~/.ssh/id_ed25519 + chmod 600 ~/.ssh/id_ed25519 + + - name: Build Linux release + run: | + HASH=$(git rev-parse --short HEAD) + flutter build linux --release --no-pub --dart-define=GIT_HASH=$HASH + + - name: Deploy Linux build to server + run: | + HASH=$(git rev-parse --short HEAD) + DATE_PATH=$(date -u +%Y/%m/%d) + REMOTE_DIR="public_html/builds/$DATE_PATH" + TARBALL="sharedinbox-linux-amd64-$HASH.tar.gz" + tar -czf /tmp/$TARBALL -C build/linux/x64/release bundle + ssh -o StrictHostKeyChecking=no "$SSH_USER@$SSH_HOST" "mkdir -p $REMOTE_DIR" + scp -o StrictHostKeyChecking=no /tmp/$TARBALL "$SSH_USER@$SSH_HOST:$REMOTE_DIR/$TARBALL" + DOWNLOAD_URL="https://sharedinbox.de/builds/$DATE_PATH/$TARBALL" + EXISTING=$(ssh -o StrictHostKeyChecking=no "$SSH_USER@$SSH_HOST" \ + "cat public_html/latest.json 2>/dev/null || echo '{}'") + WINDOWS_URL=$(echo "$EXISTING" | \ + python3 -c "import json,sys; d=json.load(sys.stdin); print(d.get('windows',''))" \ + 2>/dev/null || true) + if [ -n "$WINDOWS_URL" ]; then + echo "{\"version\":\"$HASH\",\"linux\":\"$DOWNLOAD_URL\",\"windows\":\"$WINDOWS_URL\"}" | \ + ssh -o StrictHostKeyChecking=no "$SSH_USER@$SSH_HOST" "cat > public_html/latest.json" + else + echo "{\"version\":\"$HASH\",\"linux\":\"$DOWNLOAD_URL\"}" | \ + ssh -o StrictHostKeyChecking=no "$SSH_USER@$SSH_HOST" "cat > public_html/latest.json" + fi + + - name: Generate build history pages + run: python3 scripts/generate_build_history.py + + - name: Build website + env: + HUGO_PARAMS_GITVERSION: ${{ github.sha }} + run: hugo --source website --minify + + - name: Deploy website + run: | + rsync -avz --delete \ + --exclude='*.apk' \ + --exclude='*.tar.gz' \ + -e "ssh -o StrictHostKeyChecking=no" \ + website/public/ \ + "$SSH_USER@$SSH_HOST:public_html/" diff --git a/.gitignore b/.gitignore index 5e9a0dd..410edf5 100644 --- a/.gitignore +++ b/.gitignore @@ -58,6 +58,10 @@ linux/flutter/generated_plugins.cmake .flutter-plugins-dependencies .metadata +# --- Python --- +__pycache__/ +*.pyc + # --- Tools & Cache --- .fvm/ fvm/ @@ -98,6 +102,7 @@ sharedinbox-runner/runner-data/ website/public/ website/resources/ website/.hugo_build.lock +website/content/builds/_index.md website/content/builds/[0-9]*/ .copilot/ @@ -106,4 +111,5 @@ website/content/builds/[0-9]*/ .wget-hsts tmp/ +test/widget/failures/ .claude* diff --git a/Taskfile.yml b/Taskfile.yml index 4e3cbfc..0d95b35 100644 --- a/Taskfile.yml +++ b/Taskfile.yml @@ -459,7 +459,7 @@ tasks: echo "Uploaded $APK_NAME to $REMOTE_DIR" generate-build-history: - desc: Generate Hugo build-history pages from APKs on the server + desc: Generate Hugo build-history pages from Linux and Android builds on the server preconditions: - sh: test -n "$SSH_USER" msg: "SSH_USER is not set" @@ -468,6 +468,17 @@ tasks: cmds: - python3 scripts/generate_build_history.py + website-publish: + desc: Generate build history, build Hugo site, and rsync to server (requires SSH_USER + SSH_HOST) + preconditions: + - sh: test -n "$SSH_USER" + msg: "SSH_USER is not set" + - sh: test -n "$SSH_HOST" + msg: "SSH_HOST is not set" + cmds: + - task: generate-build-history + - task: website-deploy + website-deploy: desc: Deploy the website via rsync to public_html deps: [website-build] @@ -475,6 +486,7 @@ tasks: - | rsync -avz --delete \ --exclude='*.apk' \ + --exclude='*.tar.gz' \ -e "ssh -o StrictHostKeyChecking=no" \ website/public/ \ ${SSH_USER}@${SSH_HOST}:public_html/ diff --git a/scripts/generate_build_history.py b/scripts/generate_build_history.py index ca698d2..84690cb 100644 --- a/scripts/generate_build_history.py +++ b/scripts/generate_build_history.py @@ -1,10 +1,16 @@ #!/usr/bin/env python3 -"""Generate Hugo markdown pages listing Android builds fetched from the server. +"""Generate Hugo markdown pages listing builds fetched from the server. -Reads APK files under public_html/builds/ on the deployment server via SSH, +Reads build artifacts under public_html/builds/ on the deployment server via SSH, parses the git hash from each filename, fetches the commit title from the Codeberg API, then writes Hugo content pages to website/content/builds/. +Covers two platforms: + - Linux: sharedinbox-linux-amd64-.tar.gz + - Android: sharedinbox-mua-.apk + +At most MAX_BUILDS_PER_PLATFORM of the most-recent builds are shown per platform. + These generated pages are not tracked in git (see .gitignore). """ import json @@ -20,22 +26,23 @@ REMOTE_BUILDS_DIR = "public_html/builds" CONTENT_DIR = Path("website/content/builds") BASE_URL = "https://sharedinbox.de" CODEBERG_BASE = "https://codeberg.org" +MAX_BUILDS_PER_PLATFORM = 30 -def list_apks(ssh_user: str, ssh_host: str) -> list[str]: +def list_remote_files(ssh_user: str, ssh_host: str, pattern: str) -> list[str]: result = subprocess.run( [ "ssh", "-o", "StrictHostKeyChecking=no", f"{ssh_user}@{ssh_host}", - f"find {REMOTE_BUILDS_DIR} -name '*.apk' -type f | sort", + f"find {REMOTE_BUILDS_DIR} -name '{pattern}' -type f | sort", ], capture_output=True, text=True, check=True, ) - return [l.strip() for l in result.stdout.splitlines() if l.strip()] + return [line.strip() for line in result.stdout.splitlines() if line.strip()] def get_commit_info(hash_val: str) -> tuple[str, str]: @@ -53,6 +60,41 @@ def get_commit_info(hash_val: str) -> tuple[str, str]: return hash_val, "" +def parse_builds( + paths: list[str], + path_re: re.Pattern, # type: ignore[type-arg] +) -> dict[str, list[tuple[str, str, str, str]]]: + """Parse build file paths into {date_key: [(hash, url, title, dt), ...]}.""" + limited = paths[-MAX_BUILDS_PER_PLATFORM:] if len(paths) > MAX_BUILDS_PER_PLATFORM else paths + days: dict[str, list[tuple[str, str, str, str]]] = {} + for path in limited: + m = path_re.match(path) + if not m: + print(f" skipping unexpected path: {path}", file=sys.stderr) + continue + year, month, day, filename, hash_val = m.groups() + date_key = f"{year}/{month}/{day}" + download_url = f"{BASE_URL}/builds/{year}/{month}/{day}/{filename}" + commit_title, commit_dt = get_commit_info(hash_val) + days.setdefault(date_key, []).append((hash_val, download_url, commit_title, commit_dt)) + return days + + +def render_entries( + entries: list[tuple[str, str, str, str]], + link_label: str, +) -> str: + lines = [] + for hash_val, download_url, commit_title, commit_dt in entries: + commit_url = f"{CODEBERG_BASE}/{CODEBERG_REPO}/commit/{hash_val}" + dt_str = f" · {commit_dt}" if commit_dt else "" + lines.append( + f"- [{commit_title}]({commit_url}){dt_str} \n" + f" [{link_label}]({download_url}) (`{hash_val}`)\n" + ) + return "".join(lines) + + def main() -> None: ssh_user = os.environ.get("SSH_USER", "") ssh_host = os.environ.get("SSH_HOST", "") @@ -60,62 +102,74 @@ def main() -> None: print("SSH_USER and SSH_HOST must be set", file=sys.stderr) sys.exit(1) - print(f"Listing APKs on {ssh_host}…") - apk_paths = list_apks(ssh_user, ssh_host) - print(f"Found {len(apk_paths)} APK(s)") + print(f"Listing Linux builds on {ssh_host}…") + linux_paths = list_remote_files(ssh_user, ssh_host, "sharedinbox-linux-amd64-*.tar.gz") + print(f"Found {len(linux_paths)} Linux build(s)") + linux_re = re.compile( + r"public_html/builds/(\d{4})/(\d{2})/(\d{2})/(sharedinbox-linux-amd64-(.+)\.tar\.gz)$" + ) + linux_days = parse_builds(linux_paths, linux_re) - # Group by YYYY/MM/DD - days: dict[str, list[tuple[str, str, str]]] = {} - for apk_path in apk_paths: - m = re.match( - r"public_html/builds/(\d{4})/(\d{2})/(\d{2})/(sharedinbox-mua-(.+)\.apk)$", - apk_path, - ) - if not m: - print(f" skipping unexpected path: {apk_path}", file=sys.stderr) - continue - year, month, day, filename, hash_val = m.groups() - date_key = f"{year}/{month}/{day}" - download_url = f"{BASE_URL}/builds/{year}/{month}/{day}/{filename}" - commit_title, commit_dt = get_commit_info(hash_val) - days.setdefault(date_key, []).append((hash_val, download_url, commit_title, commit_dt)) + print(f"Listing Android APKs on {ssh_host}…") + apk_paths = list_remote_files(ssh_user, ssh_host, "*.apk") + print(f"Found {len(apk_paths)} APK(s)") + apk_re = re.compile( + r"public_html/builds/(\d{4})/(\d{2})/(\d{2})/(sharedinbox-mua-(.+)\.apk)$" + ) + android_days = parse_builds(apk_paths, apk_re) CONTENT_DIR.mkdir(parents=True, exist_ok=True) - # _index.md: flat list of all builds, newest first + # _index.md: platform sections, newest-first within each index_lines = ["---\ntitle: Builds\n---\n\n"] - for date_key in sorted(days, reverse=True): - year, month, day = date_key.split("/") - date_iso = f"{year}-{month}-{day}" - index_lines.append(f"## {date_iso}\n\n") - for hash_val, download_url, commit_title, commit_dt in days[date_key]: - commit_url = f"{CODEBERG_BASE}/{CODEBERG_REPO}/commit/{hash_val}" - dt_str = f" · {commit_dt}" if commit_dt else "" - index_lines.append( - f"- [{commit_title}]({commit_url}){dt_str} \n" - f" [Download APK]({download_url}) (`{hash_val}`)\n" - ) - index_lines.append("\n") + + index_lines.append(f"## Linux (last {MAX_BUILDS_PER_PLATFORM})\n\n") + if linux_days: + for date_key in sorted(linux_days, reverse=True): + year, month, day = date_key.split("/") + index_lines.append(f"### {year}-{month}-{day}\n\n") + index_lines.append(render_entries(linux_days[date_key], "Download")) + index_lines.append("\n") + else: + index_lines.append("_No Linux builds yet._\n\n") + + index_lines.append(f"## Android (last {MAX_BUILDS_PER_PLATFORM})\n\n") + if android_days: + for date_key in sorted(android_days, reverse=True): + year, month, day = date_key.split("/") + index_lines.append(f"### {year}-{month}-{day}\n\n") + index_lines.append(render_entries(android_days[date_key], "Download APK")) + index_lines.append("\n") + else: + index_lines.append("_No Android builds yet._\n\n") + (CONTENT_DIR / "_index.md").write_text("".join(index_lines), encoding="utf-8") - for date_key in sorted(days): + # Per-day pages (combined) + all_days = set(linux_days) | set(android_days) + for date_key in sorted(all_days): year, month, day = date_key.split("/") date_iso = f"{year}-{month}-{day}" day_dir = CONTENT_DIR / year / month day_dir.mkdir(parents=True, exist_ok=True) lines = [f"---\ntitle: 'Builds for {date_iso}'\ndate: {date_iso}T00:00:00Z\n---\n\n"] - for hash_val, download_url, commit_title, commit_dt in days[date_key]: - commit_url = f"{CODEBERG_BASE}/{CODEBERG_REPO}/commit/{hash_val}" - dt_str = f" · {commit_dt}" if commit_dt else "" - lines.append( - f"- [{commit_title}]({commit_url}){dt_str} \n" - f" [Download APK]({download_url}) (`{hash_val}`)\n" - ) + if date_key in linux_days: + lines.append("## Linux\n\n") + lines.append(render_entries(linux_days[date_key], "Download")) + lines.append("\n") + if date_key in android_days: + lines.append("## Android\n\n") + lines.append(render_entries(android_days[date_key], "Download APK")) + lines.append("\n") (day_dir / f"{day}.md").write_text("".join(lines), encoding="utf-8") - total_builds = sum(len(v) for v in days.values()) - print(f"Generated {len(days)} day page(s) covering {total_builds} build(s)") + total_linux = sum(len(v) for v in linux_days.values()) + total_android = sum(len(v) for v in android_days.values()) + print( + f"Generated pages: {total_linux} Linux build(s), {total_android} Android build(s) " + f"across {len(all_days)} day(s)" + ) if __name__ == "__main__": diff --git a/scripts/test_generate_build_history.py b/scripts/test_generate_build_history.py new file mode 100644 index 0000000..7eb4c80 --- /dev/null +++ b/scripts/test_generate_build_history.py @@ -0,0 +1,113 @@ +#!/usr/bin/env python3 +"""Tests for pure functions in generate_build_history.py.""" +import re +import unittest +from unittest.mock import patch + +from generate_build_history import MAX_BUILDS_PER_PLATFORM, parse_builds, render_entries + +LINUX_RE = re.compile( + r"public_html/builds/(\d{4})/(\d{2})/(\d{2})/(sharedinbox-linux-amd64-(.+)\.tar\.gz)$" +) +APK_RE = re.compile( + r"public_html/builds/(\d{4})/(\d{2})/(\d{2})/(sharedinbox-mua-(.+)\.apk)$" +) + + +def _fake_commit_info(hash_val: str): + return (f"feat: {hash_val}", "2025-05-10T12:00:00Z") + + +class TestParseBuilds(unittest.TestCase): + def setUp(self): + patcher = patch("generate_build_history.get_commit_info", side_effect=_fake_commit_info) + self.mock_commit = patcher.start() + self.addCleanup(patcher.stop) + + def test_linux_path_parsed(self): + paths = ["public_html/builds/2025/05/10/sharedinbox-linux-amd64-abc1234.tar.gz"] + result = parse_builds(paths, LINUX_RE) + self.assertIn("2025/05/10", result) + entry = result["2025/05/10"][0] + self.assertEqual(entry[0], "abc1234") + self.assertIn("sharedinbox-linux-amd64-abc1234.tar.gz", entry[1]) + + def test_apk_path_parsed(self): + paths = ["public_html/builds/2025/05/11/sharedinbox-mua-def5678.apk"] + result = parse_builds(paths, APK_RE) + self.assertIn("2025/05/11", result) + entry = result["2025/05/11"][0] + self.assertEqual(entry[0], "def5678") + self.assertIn("sharedinbox-mua-def5678.apk", entry[1]) + + def test_unexpected_path_skipped(self): + paths = [ + "public_html/builds/2025/05/10/sharedinbox-linux-amd64-abc1234.tar.gz", + "public_html/builds/bad-path/other.tar.gz", + ] + result = parse_builds(paths, LINUX_RE) + self.assertEqual(len(result), 1) + + def test_multiple_builds_same_day(self): + paths = [ + "public_html/builds/2025/05/10/sharedinbox-linux-amd64-aaa0001.tar.gz", + "public_html/builds/2025/05/10/sharedinbox-linux-amd64-bbb0002.tar.gz", + ] + result = parse_builds(paths, LINUX_RE) + self.assertEqual(len(result["2025/05/10"]), 2) + + def test_limited_to_max_builds(self): + paths = [ + f"public_html/builds/2025/05/{i:02d}/sharedinbox-linux-amd64-hash{i:03d}.tar.gz" + for i in range(1, MAX_BUILDS_PER_PLATFORM + 5) + ] + result = parse_builds(paths, LINUX_RE) + total = sum(len(v) for v in result.values()) + self.assertEqual(total, MAX_BUILDS_PER_PLATFORM) + + def test_download_url_contains_date_and_filename(self): + paths = ["public_html/builds/2025/03/15/sharedinbox-linux-amd64-cafebabe.tar.gz"] + result = parse_builds(paths, LINUX_RE) + url = result["2025/03/15"][0][1] + self.assertIn("/2025/03/15/", url) + self.assertIn("sharedinbox-linux-amd64-cafebabe.tar.gz", url) + self.assertTrue(url.startswith("https://")) + + +class TestRenderEntries(unittest.TestCase): + def _make_entry(self, hash_val="abc1234", url="https://example.com/file.apk", + title="feat: something", dt="2025-05-10T12:00:00Z"): + return (hash_val, url, title, dt) + + def test_output_contains_title_and_link(self): + entry = self._make_entry() + out = render_entries([entry], "Download APK") + self.assertIn("feat: something", out) + self.assertIn("Download APK", out) + self.assertIn("abc1234", out) + + def test_commit_url_uses_hash(self): + entry = self._make_entry(hash_val="deadbeef") + out = render_entries([entry], "Download") + self.assertIn("deadbeef", out) + self.assertIn("codeberg.org", out) + + def test_datetime_shown_when_present(self): + entry = self._make_entry(dt="2025-05-10T12:00:00Z") + out = render_entries([entry], "Download") + self.assertIn("2025-05-10T12:00:00Z", out) + + def test_datetime_omitted_when_empty(self): + entry = self._make_entry(dt="") + out = render_entries([entry], "Download") + self.assertNotIn(" · ", out) + + def test_multiple_entries_all_rendered(self): + entries = [self._make_entry(hash_val=f"hash{i}", title=f"commit {i}") for i in range(3)] + out = render_entries(entries, "Download") + for i in range(3): + self.assertIn(f"commit {i}", out) + + +if __name__ == "__main__": + unittest.main() diff --git a/stalwart-dev/integration_ui_test.sh b/stalwart-dev/integration_ui_test.sh index 83bb68c..b287ea0 100755 --- a/stalwart-dev/integration_ui_test.sh +++ b/stalwart-dev/integration_ui_test.sh @@ -46,7 +46,7 @@ command -v xvfb-run >/dev/null || { # but the leftover Xvfb's stale /tmp/.X11-unix/X socket and lock file confuse # its cleanup, producing "kill: No such process" on exit and a non-zero status # even when the test itself passed. -for _xvfb_pid in $(pgrep -u "$USER" -x Xvfb 2>/dev/null); do +for _xvfb_pid in $(pgrep -u "${USER:-$(id -un)}" -x Xvfb 2>/dev/null); do _xvfb_display=$(tr '\0' ' ' < "/proc/${_xvfb_pid}/cmdline" 2>/dev/null \ | grep -oE ':[0-9]+' | head -1) kill "$_xvfb_pid" 2>/dev/null || true @@ -109,8 +109,8 @@ ts "flutter test start" # Stale processes can hold onto the Xvfb display, causing the new Flutter app # to hang indefinitely during GTK initialisation without ever connecting back # to the flutter test runner. -pkill -u "$USER" -f "sharedinbox" 2>/dev/null || true -pkill -u "$USER" -f "flutter.*integration" 2>/dev/null || true +pkill -u "${USER:-$(id -un)}" -f "sharedinbox" 2>/dev/null || true +pkill -u "${USER:-$(id -un)}" -f "flutter.*integration" 2>/dev/null || true sleep 1 # Find an unused display number. @@ -145,7 +145,7 @@ for _attempt in 1 2; do [ "$_e2e_exit" -eq 0 ] && break || true if [ $_attempt -lt 2 ]; then ts "E2E attempt $_attempt failed (exit $_e2e_exit), restarting Xvfb and retrying..." - pkill -u "$USER" -f "sharedinbox" 2>/dev/null || true + pkill -u "${USER:-$(id -un)}" -f "sharedinbox" 2>/dev/null || true # Kill the old Xvfb and start a fresh one on a new display number. kill "${XVFB_PID:-}" 2>/dev/null || true wait "${XVFB_PID:-}" 2>/dev/null || true