From 54f55840525c35515cef26ea534da208e217b2c1 Mon Sep 17 00:00:00 2001 From: Aswin C Date: Sat, 10 Oct 2026 06:27:50 +0530 Subject: [PATCH 1/9] feat(updates): add verified updates and reviewed announcements --- .github/PULL_REQUEST_TEMPLATE.md | 15 +- .github/scripts/generate_update_manifest.py | 113 +++ .github/scripts/manual_dispatch.py | 225 +++-- .github/scripts/prepare_release.py | 42 +- .../scripts/preview_announcement_on_device.py | 45 + .github/scripts/refresh_upcoming.py | 22 + .github/scripts/rehearse_release.py | 89 ++ .github/scripts/release_classfile.py | 9 +- .../scripts/release_verification.init.gradle | 1 + .github/scripts/serve_update_test.py | 80 ++ .github/scripts/sync_announcement_preview.py | 23 + .github/scripts/test_announcement_admin.cjs | 39 + .github/scripts/test_announcements.py | 140 +++ .github/scripts/test_manual_dispatch.py | 98 ++ .github/scripts/test_release_contracts.py | 5 +- .../scripts/test_release_copy_validation.py | 152 +++ .github/scripts/test_release_rehearsal.py | 93 ++ .github/scripts/test_update_changelog_copy.py | 68 +- .github/scripts/test_update_manifest.py | 74 ++ .github/scripts/update_changelog.py | 903 +++--------------- .github/scripts/verify_release_contracts.py | 23 +- .github/workflows/changelog-on-merge.yml | 107 ++- .github/workflows/release-contracts.yml | 13 +- .github/workflows/release-copy.yml | 23 + .github/workflows/release-rehearsal.yml | 61 ++ .github/workflows/send-notification.yml | 118 ++- .github/workflows/unit-tests.yml | 6 + .gitignore | 13 +- admin-panel/README.md | 19 + .../notifyappusers/announcements/font.ttf | Bin 0 -> 4153300 bytes .../notifyappusers/announcements/renderer.css | 21 + .../announcements/renderer.html | 9 + .../notifyappusers/announcements/renderer.js | 76 ++ .../public/notifyappusers/composer.css | 6 + .../public/notifyappusers/dashboard.html | 51 + .../public/notifyappusers/dashboard.js | 59 ++ .../public/notifyappusers/preview.html | 1 + app/README.md | 28 + app/build.gradle.kts | 46 + app/src/direct/AndroidManifest.xml | 4 + .../boxlore/updates/ApkFileDownloader.kt | 100 ++ .../cx/aswin/boxlore/updates/ApkIdentity.kt | 19 + .../boxlore/updates/DirectApkInstaller.kt | 71 ++ .../java/cx/aswin/boxlore/AppContainer.kt | 1 + .../cx/aswin/boxlore/BoxLoreApplication.kt | 4 +- .../java/cx/aswin/boxlore/MainActivity.kt | 10 +- .../aswin/boxlore/fcm/AnnouncementPolicy.kt | 77 ++ .../cx/aswin/boxlore/fcm/BoxLoreFcmService.kt | 130 +-- .../cx/aswin/boxlore/fcm/FcmPayloadParser.kt | 16 + .../cx/aswin/boxlore/fcm/FcmTopicHelper.kt | 26 +- .../NavGraphSettingsDestinations.kt | 1 + .../boxlore/navigation/PushActionRouter.kt | 53 + .../navigation/PushTargetRouteAllowlist.kt | 5 + .../cx/aswin/boxlore/ui/BoxLoreAppRoot.kt | 92 +- .../boxlore/ui/NativeDialogSystemBars.kt | 25 + .../ui/announcement/AnnouncementActions.kt | 26 + .../announcement/InAppAnnouncementDialog.kt | 426 ++------- .../cx/aswin/boxlore/updates/ApkInstaller.kt | 17 + .../cx/aswin/boxlore/updates/AppUpdates.kt | 251 +++++ .../aswin/boxlore/updates/AppUpdatesHost.kt | 28 + .../aswin/boxlore/updates/AppUpdatesPanel.kt | 223 +++++ .../boxlore/updates/DirectUpdateSource.kt | 43 + .../boxlore/updates/PlayAppUpdateHelper.kt | 56 +- .../aswin/boxlore/updates/PlayUpdateSource.kt | 24 + .../boxlore/updates/ReleaseNotesMarkdown.kt | 113 +++ .../cx/aswin/boxlore/updates/UpdateChecker.kt | 99 ++ .../aswin/boxlore/updates/UpdateHttpCall.kt | 32 + .../aswin/boxlore/updates/UpdateManifest.kt | 74 ++ .../res/drawable/ic_boxlore_wordmark_mic.xml | 10 + app/src/main/res/values/announcements.xml | 9 + app/src/main/res/values/updates.xml | 27 + app/src/main/res/xml/backup_rules.xml | 6 + .../main/res/xml/data_extraction_rules.xml | 12 + .../aswin/boxlore/updates/PlayApkInstaller.kt | 11 + .../boxlore/fcm/AnnouncementPolicyTest.kt | 53 + .../boxlore/fcm/PushAnnouncementActionTest.kt | 56 ++ .../navigation/PushActionRouterTest.kt | 145 +++ .../boxlore/ui/NativeDialogSystemBarsTest.kt | 15 + .../ui/announcement/AnnouncementRenderTest.kt | 55 ++ .../boxlore/updates/ApkFileDownloaderTest.kt | 106 ++ .../aswin/boxlore/updates/ApkIdentityTest.kt | 29 + .../boxlore/updates/AppUpdatesPanelTest.kt | 181 ++++ .../aswin/boxlore/updates/AppUpdatesTest.kt | 274 ++++++ .../boxlore/updates/DirectUpdateSourceTest.kt | 40 + .../boxlore/updates/PlayUpdatePolicyTest.kt | 15 + .../updates/ReleaseNotesMarkdownTest.kt | 36 + .../boxlore/updates/UpdateCheckerTest.kt | 198 ++++ .../boxlore/updates/UpdateEntryPointsTest.kt | 60 ++ .../boxlore/updates/UpdateHttpCallTest.kt | 51 + .../boxlore/updates/UpdateManifestTest.kt | 40 + .../boxlore/updates/UpdateSessionTest.kt | 139 +++ app/src/updateTest/AndroidManifest.xml | 10 + .../testing/AnnouncementTestReceiver.kt | 33 + .../res/xml/update_test_network.xml | 4 + core/designsystem/README.md | 6 +- .../components/TopBarUtilityActions.kt | 99 +- .../components/UpdateAvailableIcon.kt | 58 ++ .../src/main/res/values/updates.xml | 3 + core/prefs/README.md | 2 + .../core/prefs/UserPreferencesRepository.kt | 42 + .../core/prefs/AnnouncementPreferencesTest.kt | 93 ++ .../prefs/UserPreferencesRepositoryTest.kt | 45 - docs/TESTING.md | 46 +- feature/settings/README.md | 2 + .../feature/settings/SettingsScreen.kt | 3 + .../settings/pages/AboutSettingsPage.kt | 10 + .../feature/settings/pages/SettingsHub.kt | 12 + .../settings/src/main/res/values/updates.xml | 3 + scripts/README.md | 43 +- 109 files changed, 5500 insertions(+), 1544 deletions(-) create mode 100644 .github/scripts/generate_update_manifest.py create mode 100644 .github/scripts/preview_announcement_on_device.py create mode 100644 .github/scripts/refresh_upcoming.py create mode 100644 .github/scripts/rehearse_release.py create mode 100644 .github/scripts/serve_update_test.py create mode 100644 .github/scripts/sync_announcement_preview.py create mode 100644 .github/scripts/test_announcement_admin.cjs create mode 100644 .github/scripts/test_announcements.py create mode 100644 .github/scripts/test_manual_dispatch.py create mode 100644 .github/scripts/test_release_copy_validation.py create mode 100644 .github/scripts/test_release_rehearsal.py create mode 100644 .github/scripts/test_update_manifest.py create mode 100644 .github/workflows/release-copy.yml create mode 100644 .github/workflows/release-rehearsal.yml create mode 100644 admin-panel/README.md create mode 100644 admin-panel/public/notifyappusers/announcements/font.ttf create mode 100644 admin-panel/public/notifyappusers/announcements/renderer.css create mode 100644 admin-panel/public/notifyappusers/announcements/renderer.html create mode 100644 admin-panel/public/notifyappusers/announcements/renderer.js create mode 100644 admin-panel/public/notifyappusers/composer.css create mode 100644 admin-panel/public/notifyappusers/dashboard.html create mode 100644 admin-panel/public/notifyappusers/dashboard.js create mode 100644 admin-panel/public/notifyappusers/preview.html create mode 100644 app/src/direct/AndroidManifest.xml create mode 100644 app/src/direct/java/cx/aswin/boxlore/updates/ApkFileDownloader.kt create mode 100644 app/src/direct/java/cx/aswin/boxlore/updates/ApkIdentity.kt create mode 100644 app/src/direct/java/cx/aswin/boxlore/updates/DirectApkInstaller.kt create mode 100644 app/src/main/java/cx/aswin/boxlore/fcm/AnnouncementPolicy.kt create mode 100644 app/src/main/java/cx/aswin/boxlore/navigation/PushActionRouter.kt create mode 100644 app/src/main/java/cx/aswin/boxlore/ui/NativeDialogSystemBars.kt create mode 100644 app/src/main/java/cx/aswin/boxlore/ui/announcement/AnnouncementActions.kt create mode 100644 app/src/main/java/cx/aswin/boxlore/updates/ApkInstaller.kt create mode 100644 app/src/main/java/cx/aswin/boxlore/updates/AppUpdates.kt create mode 100644 app/src/main/java/cx/aswin/boxlore/updates/AppUpdatesHost.kt create mode 100644 app/src/main/java/cx/aswin/boxlore/updates/AppUpdatesPanel.kt create mode 100644 app/src/main/java/cx/aswin/boxlore/updates/DirectUpdateSource.kt create mode 100644 app/src/main/java/cx/aswin/boxlore/updates/PlayUpdateSource.kt create mode 100644 app/src/main/java/cx/aswin/boxlore/updates/ReleaseNotesMarkdown.kt create mode 100644 app/src/main/java/cx/aswin/boxlore/updates/UpdateChecker.kt create mode 100644 app/src/main/java/cx/aswin/boxlore/updates/UpdateHttpCall.kt create mode 100644 app/src/main/java/cx/aswin/boxlore/updates/UpdateManifest.kt create mode 100644 app/src/main/res/drawable/ic_boxlore_wordmark_mic.xml create mode 100644 app/src/main/res/values/announcements.xml create mode 100644 app/src/main/res/values/updates.xml create mode 100644 app/src/playRelease/java/cx/aswin/boxlore/updates/PlayApkInstaller.kt create mode 100644 app/src/test/java/cx/aswin/boxlore/fcm/AnnouncementPolicyTest.kt create mode 100644 app/src/test/java/cx/aswin/boxlore/fcm/PushAnnouncementActionTest.kt create mode 100644 app/src/test/java/cx/aswin/boxlore/navigation/PushActionRouterTest.kt create mode 100644 app/src/test/java/cx/aswin/boxlore/ui/NativeDialogSystemBarsTest.kt create mode 100644 app/src/test/java/cx/aswin/boxlore/ui/announcement/AnnouncementRenderTest.kt create mode 100644 app/src/test/java/cx/aswin/boxlore/updates/ApkFileDownloaderTest.kt create mode 100644 app/src/test/java/cx/aswin/boxlore/updates/ApkIdentityTest.kt create mode 100644 app/src/test/java/cx/aswin/boxlore/updates/AppUpdatesPanelTest.kt create mode 100644 app/src/test/java/cx/aswin/boxlore/updates/AppUpdatesTest.kt create mode 100644 app/src/test/java/cx/aswin/boxlore/updates/DirectUpdateSourceTest.kt create mode 100644 app/src/test/java/cx/aswin/boxlore/updates/PlayUpdatePolicyTest.kt create mode 100644 app/src/test/java/cx/aswin/boxlore/updates/ReleaseNotesMarkdownTest.kt create mode 100644 app/src/test/java/cx/aswin/boxlore/updates/UpdateCheckerTest.kt create mode 100644 app/src/test/java/cx/aswin/boxlore/updates/UpdateEntryPointsTest.kt create mode 100644 app/src/test/java/cx/aswin/boxlore/updates/UpdateHttpCallTest.kt create mode 100644 app/src/test/java/cx/aswin/boxlore/updates/UpdateManifestTest.kt create mode 100644 app/src/test/java/cx/aswin/boxlore/updates/UpdateSessionTest.kt create mode 100644 app/src/updateTest/AndroidManifest.xml create mode 100644 app/src/updateTest/java/cx/aswin/boxlore/testing/AnnouncementTestReceiver.kt create mode 100644 app/src/updateTest/res/xml/update_test_network.xml create mode 100644 core/designsystem/src/main/java/cx/aswin/boxlore/core/designsystem/components/UpdateAvailableIcon.kt create mode 100644 core/designsystem/src/main/res/values/updates.xml create mode 100644 core/prefs/src/test/java/cx/aswin/boxlore/core/prefs/AnnouncementPreferencesTest.kt create mode 100644 feature/settings/src/main/res/values/updates.xml diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index e2a54e4e2..088fe53da 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -96,17 +96,18 @@ Add impact labels on the PR (`gh pr edit --add-label user-impact-critical -- -### CHANGELOG.md (developer copy) +### CHANGELOG.md (required developer copy) Keep a Changelog bullets for engineers. Use `### Added` / `### Changed` / `### Fixed` when you have more than one category. Class/module names are OK here. @@ -117,7 +118,7 @@ Keep a Changelog bullets for engineers. Use `### Added` / `### Changed` / `### F -### README What's New / Upcoming (listener copy) +### README What's New / Upcoming (required for user-impact changes) Plain listener English for README Upcoming, then What's New on release, and in-app release notes. Product name is **boxlore** (lowercase). No class names, FCM keys, or CI. Use functional categories: `### Critical` (for `user-impact-critical`), `### Improvements`, or `### Fixes`. Do not use impact levels like `### High` or `### Medium` as headings. diff --git a/.github/scripts/generate_update_manifest.py b/.github/scripts/generate_update_manifest.py new file mode 100644 index 000000000..24c1b1135 --- /dev/null +++ b/.github/scripts/generate_update_manifest.py @@ -0,0 +1,113 @@ +#!/usr/bin/env python3 +"""Create the direct-update manifest from already-verified release inputs. No publication.""" + +from __future__ import annotations + +import argparse +import hashlib +import html +import json +import re +from pathlib import Path + +from prepare_release import read_app_version +import update_changelog + + +def listener_markdown(fragment: str) -> str: + """Preserve authored emphasis and links from the generated listener HTML.""" + fragment = re.sub(r']*href=[\'"]([^\'"]+)[\'"][^>]*>(.*?)', + lambda m: '[' + re.sub(r'<[^>]+>', '', m[2]) + '](' + html.unescape(m[1]) + ')', + fragment, flags=re.DOTALL | re.IGNORECASE) + for tag, marker in (("b", "**"), ("strong", "**"), ("i", "*"), ("em", "*"), ("code", "`")): + fragment = re.sub(r']*>', lambda _: marker, fragment, flags=re.IGNORECASE) + fragment = re.sub(r'', '\n', fragment, flags=re.IGNORECASE) + return html.unescape(re.sub(r'<[^>]+>', '', fragment)).strip() + + +def listener_notes(readme: str, tag: str | None = None) -> str: + region = update_changelog._extract_marked_region( + readme, update_changelog.RELEASE_WHATS_NEW_START, + update_changelog.RELEASE_WHATS_NEW_END, + ) + if not region: + return "" + metadata = update_changelog._parse_release_meta(region) + if tag and metadata and metadata[0] != tag: + return "" # An artifacts-only release must not claim the previous release's notes. + # Preserve every authored listener bullet, without shortening or rewriting. + return "\n".join("- " + listener_markdown(bullet) for bullet in re.findall( + r"]*>(.*?)", region, flags=re.DOTALL, + )) + + +def upcoming_notes(readme: str) -> str: + region = update_changelog._extract_marked_region( + readme, update_changelog.RELEASE_UPCOMING_START, update_changelog.RELEASE_UPCOMING_END, + ) or "" + parts = [] + for match in re.finditer(r']*>(.*?)|]*>(.*?)', region, flags=re.DOTALL): + if match[1] is not None: + parts.append("\n## " + listener_markdown(match[1]).rstrip(":")) + else: + parts.append("- " + listener_markdown(match[2])) + value = "\n".join(parts).strip() if " 6_000: + raise ValueError("Upcoming preview exceeds 6,000 characters") + return value + + +def build_manifest(gradle: str, apk: Path, asset: str, notes: str = "") -> dict: + version = read_app_version(gradle) + if not re.fullmatch(r"[A-Za-z0-9_.-]+\.apk", asset): + raise ValueError("APK asset must be a plain .apk filename") + sdk = re.search(r"\bminSdk\s*=\s*(\d+)", gradle) + if sdk is None or int(sdk.group(1)) < 31: + raise ValueError("Could not resolve supported minimum Android version") + size = apk.stat().st_size + if not 0 < size <= 512 * 1024 * 1024: + raise ValueError("APK is missing, empty or larger than the updater limit") + if len(notes) > 24_000: + raise ValueError("Release notes exceed 24,000 characters; supply concise reviewed copy") + with apk.open("rb") as binary: + digest = hashlib.file_digest(binary, "sha256").hexdigest() + base = "https://github.com/boxcreate/boxlore/releases" + manifest = { + "schemaVersion": 1, "packageName": "cx.aswin.boxlore", + "versionCode": version.code, "versionName": version.name, + "minSdk": int(sdk.group(1)), "apkUrl": f"{base}/download/{version.tag}/{asset}", + "apkSha256": digest, "apkBytes": size, "notesUrl": f"{base}/tag/{version.tag}", + "notes": notes, + } + if len(encode_manifest(manifest)) > 64 * 1024: + raise ValueError("Update manifest exceeds 64 KiB; shorten the reviewed release notes") + return manifest + + +def encode_manifest(manifest: dict) -> bytes: + return (json.dumps(manifest, indent=2, ensure_ascii=False) + "\n").encode("utf-8") + + +def main() -> None: + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--apk", required=True, type=Path) + parser.add_argument("--asset", required=True) + parser.add_argument("--gradle", type=Path, default=Path("app/build.gradle.kts")) + parser.add_argument("--readme", type=Path, default=Path("README.md")) + parser.add_argument("--output", required=True, type=Path) + args = parser.parse_args() + try: + gradle = args.gradle.read_text() + manifest = build_manifest(gradle, args.apk, args.asset, + listener_notes(args.readme.read_text(), read_app_version(gradle).tag)) + manifest["upcoming"] = upcoming_notes(args.readme.read_text()) + encoded = encode_manifest(manifest) + if len(encoded) > 64 * 1024: + raise ValueError("Update manifest exceeds 64 KiB with upcoming preview") + args.output.write_bytes(encoded) + except (OSError, ValueError) as error: + parser.exit(1, f"Update manifest not prepared: {error}. Fix the candidate and rerun publication; no update announcement was sent.\n") + + +if __name__ == "__main__": + main() diff --git a/.github/scripts/manual_dispatch.py b/.github/scripts/manual_dispatch.py index 1239f251e..85da88e17 100644 --- a/.github/scripts/manual_dispatch.py +++ b/.github/scripts/manual_dispatch.py @@ -1,77 +1,164 @@ +"""Preview, validate or send a legacy-compatible manual announcement. + +Preview mode has no Firebase dependency. Test mode only accepts debug_users or test_users; +it never silently redirects a request intended for a production audience. +""" + import argparse -import os +import hashlib import json -import firebase_admin -from firebase_admin import credentials -from firebase_admin import messaging +import os +import sys +from pathlib import Path +from urllib.parse import urlparse -def send_notification(args): - creds_json = os.environ.get('FIREBASE_CREDENTIALS') - if not creds_json: - raise ValueError("FIREBASE_CREDENTIALS environment variable is not set") - - cred_dict = json.loads(creds_json) - cred = credentials.Certificate(cred_dict) - firebase_admin.initialize_app(cred) - - decoded_body = args.body.replace('\\n', '\n') - - data_payload = { - 'title': args.title, - 'body': decoded_body, - 'type': args.type, - 'sound': getattr(args, 'sound', 'default'), - 'action_label': getattr(args, 'action_label', 'View'), - 'show_action_in_push': getattr(args, 'show_action_in_push', 'true'), - 'show_action_in_app': getattr(args, 'show_action_in_app', 'true'), - 'category': getattr(args, 'category', "WHAT'S NEW") +TOPIC_PAYLOAD_LIMIT = 2048 # FCM counts UTF-8 data keys and values for topics. +AUDIENCES = ("all_users", "debug_users", "prod_users", "test_users", "direct_users") + + +def boolean(value, field): + if value not in ("true", "false"): + raise ValueError(f"{field} must be true or false") + return value == "true" + + +def notification_plan(args): + """Pure validation and payload construction, shared by previews and sends.""" + topic = getattr(args, "target", "all_users") + if topic not in AUDIENCES: + raise ValueError("Choose a supported announcement audience") + test_mode = boolean(getattr(args, "test_mode", "false"), "test_mode") + if test_mode and topic not in ("debug_users", "test_users"): + raise ValueError("Test mode only permits debug_users or test_users. Change the target before retrying.") + if args.type not in ("push", "in-app", "both"): + raise ValueError("Message type must be push, in-app or both") + if not args.title.strip() or not args.body.strip(): + raise ValueError("Add a title and message before previewing or sending") + if getattr(args, "image", ""): + image = urlparse(args.image) + if image.scheme != "https" or not image.netloc or image.username or image.password: + raise ValueError("Use an HTTPS image URL without embedded credentials") + sound = getattr(args, "sound", "default") + if sound not in ("default", "chime", "announcement", "silent"): + raise ValueError("Choose a supported notification sound") + data = { + "title": args.title, + "body": args.body.replace("\\n", "\n"), + "type": args.type, + "sound": sound, + "action_label": getattr(args, "action_label", "View"), + "show_action_in_push": getattr(args, "show_action_in_push", "true"), + "show_action_in_app": getattr(args, "show_action_in_app", "true"), + "category": getattr(args, "category", "ANNOUNCEMENT"), } - - if args.route: - data_payload['route'] = args.route - - if getattr(args, 'image', None): - data_payload['image'] = args.image - - topic = getattr(args, 'target', 'all_users') - - android_config = None - collapse_key = getattr(args, 'collapse_key', '') - if collapse_key: - android_config = messaging.AndroidConfig(collapse_key=collapse_key) - - message = messaging.Message( - data=data_payload, - topic=topic, - android=android_config + for field in ("show_action_in_push", "show_action_in_app"): + boolean(data[field], field) + if getattr(args, "release_url", ""): + url = urlparse(args.release_url) + if url.scheme != "https" or url.netloc != "github.com" or url.query or url.fragment or not (url.path == "/boxcreate/boxlore/releases/latest" or url.path.startswith("/boxcreate/boxlore/releases/tag/")): + raise ValueError("Use a boxlore GitHub release page") + for field in ("route", "image"): + if getattr(args, field, ""): + data[field] = getattr(args, field) + for field, choices, default in ( + ("presentation", ("compact", "fullscreen"), "compact"), + ("tone", ("primary", "secondary", "tertiary", "error"), "primary"), + ("image_style", ("banner", "cover"), "banner"), + ): + value = getattr(args, field, default) + if value not in choices: raise ValueError(f"Choose a supported {field}") + data[field] = value + for field in ("release_alert", "include_play"): + data[field] = str(boolean(getattr(args, field, "false"), field)).lower() + if data["release_alert"] == "true" and getattr(args, "release_url", ""): + data["release_url"] = args.release_url + data["test_mode"] = str(test_mode).lower() + code = getattr(args, "release_version_code", "0") + if not str(code).isdigit() or int(code) > 2147483647: raise ValueError("Release version code must be a nonnegative whole number") + data["release_version_code"] = str(int(code)) + if topic == "direct_users" and data["include_play"] == "true": + raise ValueError("Choose all_users to include Google Play installs") + if data["release_alert"] == "true" and topic not in ("direct_users", "debug_users", "test_users") and data["include_play"] != "true": + raise ValueError("Release alerts must target direct_users unless Google Play is explicitly included") + size = sum(len(key.encode("utf-8")) + len(value.encode("utf-8")) for key, value in data.items()) + if size > TOPIC_PAYLOAD_LIMIT: + raise ValueError( + f"Message data uses {size} bytes; topic messages allow {TOPIC_PAYLOAD_LIMIT}. " + "Shorten the message or link to the full notes, then preview again." + ) + return { + "topic": topic, + "data": data, + "collapse_key": getattr(args, "collapse_key", ""), + "dry_run": boolean(getattr(args, "dry_run", "false"), "dry_run"), + "test_mode": test_mode, + "payload_bytes": size, + } + + +def plan_digest(plan): + return hashlib.sha256(json.dumps(plan, ensure_ascii=False, sort_keys=True, separators=(",", ":")).encode()).hexdigest() + + +def _firebase_send(plan): + # Only the sending path imports/initializes Firebase. Offline previews cannot send. + import firebase_admin + from firebase_admin import credentials, messaging + + creds_json = os.environ.get("FIREBASE_CREDENTIALS") + if not creds_json: + raise ValueError("FIREBASE_CREDENTIALS is required for provider validation or sending") + firebase_admin.initialize_app(credentials.Certificate(json.loads(creds_json))) + android = messaging.AndroidConfig(collapse_key=plan["collapse_key"]) if plan["collapse_key"] else None + audience = {"condition": "'direct_users' in topics && !('play_users' in topics) && !('test_users' in topics)"} if plan["topic"] == "direct_users" else {"topic": plan["topic"]} + return messaging.send( + messaging.Message(data=plan["data"], android=android, **audience), + dry_run=plan["dry_run"], ) - - dry_run_bool = (getattr(args, 'dry_run', 'false') == 'true') - try: - response = messaging.send(message, dry_run=dry_run_bool) - if dry_run_bool: - print(f"Dry run validation succeeded for topic '{topic}': {response}") + + +def send_notification(args, *, sender=None): + plan = notification_plan(args) + if boolean(getattr(args, "preview_only", "false"), "preview_only"): + preview = {**plan, "approval_digest": plan_digest(plan), "mode": "offline-preview", "sent": False} + rendered = json.dumps(preview, ensure_ascii=False, indent=2) + "\n" + output = getattr(args, "preview_output", "") + if output: + Path(output).write_text(rendered, encoding="utf-8") else: - print(f"Successfully sent message to topic '{topic}': {response}") - except Exception as e: - print(f"Failed to send message: {e}") - raise e + print(rendered, end="") + return preview + if not plan["dry_run"] and getattr(args, "approval_digest", "") != plan_digest(plan): + raise ValueError("Review the current preview and approve it before sending. Edited drafts need a new approval.") + response = (sender or _firebase_send)(plan) + if plan["dry_run"]: + print(f"Provider validation passed for '{plan['topic']}'. No message sent. ID: {response}") + else: + print(f"FCM accepted the message for '{plan['topic']}'. Device delivery is not confirmed. ID: {response}") + return {**plan, "mode": "provider-validation" if plan["dry_run"] else "send", "response": response} + + +def argument_parser(): + parser = argparse.ArgumentParser(description="Preview, validate or send an FCM announcement") + parser.add_argument("--title", required=True) + parser.add_argument("--body", required=True) + parser.add_argument("--type", required=True) + for name, default in ( + ("route", ""), ("image", ""), ("target", "all_users"), + ("dry-run", "false"), ("test-mode", "false"), ("preview-only", "false"), + ("preview-output", ""), ("collapse-key", ""), ("sound", "default"), + ("presentation", "compact"), ("tone", "primary"), ("image-style", "banner"), + ("release-url", ""), ("release-alert", "false"), ("include-play", "false"), ("release-version-code", "0"), ("approval-digest", ""), + ("action-label", "View"), ("show-action-in-push", "true"), + ("show-action-in-app", "true"), ("category", "ANNOUNCEMENT"), + ): + parser.add_argument("--" + name, default=default) + return parser + if __name__ == "__main__": - parser = argparse.ArgumentParser(description='Send FCM Notification') - parser.add_argument('--title', required=True, help='Notification Title') - parser.add_argument('--body', required=True, help='Notification Body') - parser.add_argument('--type', required=True, help='Where to show (push/in-app/both)') - parser.add_argument('--route', required=False, default="", help='Optional Deep Link Route') - parser.add_argument('--image', required=False, default="", help='Optional Big Image URL') - parser.add_argument('--target', required=False, default="all_users", help='Target audience topic') - parser.add_argument('--dry-run', required=False, default="false", help='Validate only') - parser.add_argument('--collapse-key', required=False, default="", help='Optional Android collapse key') - parser.add_argument('--sound', required=False, default="default", help='Notification sound') - parser.add_argument('--action-label', required=False, default="View", help='Action Button Text') - parser.add_argument('--show-action-in-push', required=False, default="true", help='Show action in push') - parser.add_argument('--show-action-in-app', required=False, default="true", help='Show action in app') - parser.add_argument('--category', required=False, default="WHAT'S NEW", help='In-App category badge label') - - args = parser.parse_args() - send_notification(args) + try: + send_notification(argument_parser().parse_args()) + except (ValueError, OSError) as error: + print(f"Announcement stopped: {error}", file=sys.stderr) + sys.exit(1) diff --git a/.github/scripts/prepare_release.py b/.github/scripts/prepare_release.py index 9e19036c3..70c5ecc07 100644 --- a/.github/scripts/prepare_release.py +++ b/.github/scripts/prepare_release.py @@ -506,13 +506,21 @@ def pull_request_label_names(pull_request: dict[str, object]) -> list[str]: def reconcile_changelog( repository: str, token: str, - api_key: str, base_tag: str, head_sha: str, ) -> list[int]: pull_requests = pull_requests_between(repository, token, base_tag, head_sha) processed: list[int] = [] + # Reject missing copy before appending any PR in this batch. + original_unreleased = unreleased_block(require_file(CHANGELOG_PATH)) + for pull_request in pull_requests: + if not update_changelog._pr_already_present(original_unreleased, int(pull_request["number"])): + update_changelog.validate_release_copy( + str(pull_request.get("title") or ""), str(pull_request.get("body") or ""), + pull_request_label_names(pull_request), + ) + for pull_request in pull_requests: number = int(pull_request["number"]) current_unreleased = unreleased_block(require_file(CHANGELOG_PATH)) @@ -521,7 +529,6 @@ def reconcile_changelog( continue changed = update_changelog.append_changelog( - api_key, number, str(pull_request.get("title") or "").strip(), str(pull_request.get("body") or ""), @@ -530,11 +537,9 @@ def reconcile_changelog( if not changed: fail(f"PR #{number} produced no changelog entry") processed.append(number) - # Soft pacing so a long release range does not trip Groq TPM limits. - time.sleep(1.5) - update_changelog.sync_changelog_unreleased(api_key) - update_changelog.sync_readme_upcoming(api_key) + update_changelog.sync_changelog_unreleased() + update_changelog.sync_readme_upcoming() final_unreleased = unreleased_block(require_file(CHANGELOG_PATH)) missing = [ @@ -737,14 +742,8 @@ def prepare_release(args: argparse.Namespace) -> None: skip_notify=skip_notify, use_readme_upcoming=use_readme_upcoming, ) - api_key = os.environ.get("GROQ_API_KEY", "").strip() if not repository or not token: fail("GITHUB_REPOSITORY and GITHUB_TOKEN are required") - if notes_source == "reconciled" and not api_key: - fail( - "GROQ_API_KEY is required unless --skip-notify or " - "--use-readme-upcoming is set" - ) current = read_app_version() validate_baseline(current, args.latest_tag, skip_notify=skip_notify) @@ -755,11 +754,6 @@ def prepare_release(args: argparse.Namespace) -> None: processed: list[int] = [] gradle_original = require_file(APP_GRADLE_PATH) - APP_GRADLE_PATH.write_text( - replace_gradle_version(gradle_original, current, target), - encoding="utf-8", - ) - if skip_notify: # Version bump + APK badge URL only — leave CHANGELOG / Upcoming / What's New # untouched. Publish skips in-app announcement; notify manually if needed. @@ -773,22 +767,22 @@ def prepare_release(args: argparse.Namespace) -> None: processed = reconcile_changelog( repository, token, - api_key, args.latest_tag, args.head_sha, ) changelog_original = require_file(CHANGELOG_PATH) readme_original = require_file(README_PATH) - CHANGELOG_PATH.write_text( - promote_changelog(changelog_original, target, release_date), - encoding="utf-8", - ) + promoted_changelog = promote_changelog(changelog_original, target, release_date) promoted_readme = promote_readme(readme_original, target, release_date) + CHANGELOG_PATH.write_text(promoted_changelog, encoding="utf-8") README_PATH.write_text( update_readme_download_url(promoted_readme, repository, target), encoding="utf-8", ) + APP_GRADLE_PATH.write_text( + replace_gradle_version(gradle_original, current, target), encoding="utf-8", + ) write_outputs( { "current_version": current.name, @@ -1049,8 +1043,6 @@ def write_release_notes(args: argparse.Namespace) -> None: output.parent.mkdir(parents=True, exist_ok=True) output.write_text( f"# boxlore {current.tag}\n\n" - "> **AI-generated summary:** May contain mistakes. " - "Verify details against the linked pull requests.\n\n" f"{body}\n", encoding="utf-8", ) @@ -1321,7 +1313,7 @@ def main() -> None: "--use-readme-upcoming", action="store_true", help=( - "Skip AI reconciliation and promote the reviewed README Upcoming " + "Skip PR reconciliation and promote the reviewed README Upcoming " "region verbatim into What's New and the release notification" ), ) diff --git a/.github/scripts/preview_announcement_on_device.py b/.github/scripts/preview_announcement_on_device.py new file mode 100644 index 000000000..aa6c5d3af --- /dev/null +++ b/.github/scripts/preview_announcement_on_device.py @@ -0,0 +1,45 @@ +"""Render an exported announcement on an isolated updateTest build over ADB. + +No Firebase, workflow dispatch, topic message or production release is involved. +The receiver exists only in updateTest and requires Android's shell permission. +Delivery eligibility is tested separately: this local visual preview resets the +release code to 0 so an already-current test device can display the same layout. +""" +import argparse +import json +import shlex +import subprocess +from pathlib import Path + +COMPONENT = 'cx.aswin.boxlore/.testing.AnnouncementTestReceiver' +ACTION = 'cx.aswin.boxlore.TEST_ANNOUNCEMENT' + + +def preview(preview_path, adb='adb', serial=None, runner=subprocess.run): + value = json.loads(Path(preview_path).read_text(encoding='utf-8')) + data = value.get('data') + if not isinstance(data, dict) or not data.get('title') or not data.get('body'): + raise ValueError('Choose an exported announcement-preview JSON file') + command = [adb] + (['-s', serial] if serial else []) + # ADB forwards a shell command. Quote every remote argument so copy remains + # exact, including spaces, apostrophes and shell metacharacters. + remote = shlex.join(['am', 'broadcast', '-n', COMPONENT, '-a', ACTION, + '--es', 'payload', json.dumps(data, ensure_ascii=False)]) + result = runner(command + ['shell', remote], check=True, text=True, capture_output=True) + # An absent isolated receiver is a failure, not a successful preview on a normal install. + if 'result=-1' not in result.stdout or any(word in result.stdout + result.stderr for word in ('Error', 'Exception', 'Permission Denial')): + raise ValueError('Install the isolated updateTest APK first, then retry this local preview') + return result.stdout + + +if __name__ == '__main__': + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument('--preview', type=Path, required=True) + parser.add_argument('--adb', default='adb') + parser.add_argument('--serial') + args = parser.parse_args() + try: + print(preview(args.preview, args.adb, args.serial)) + print('Local preview requested. Open boxlore on the test device; no message was sent to users.') + except (ValueError, OSError, subprocess.CalledProcessError) as error: + parser.exit(1, f'Preview stopped: {error}\n') diff --git a/.github/scripts/refresh_upcoming.py b/.github/scripts/refresh_upcoming.py new file mode 100644 index 000000000..4b756b249 --- /dev/null +++ b/.github/scripts/refresh_upcoming.py @@ -0,0 +1,22 @@ +"""Refresh preview copy in a verified update manifest; never changes binary metadata.""" +import argparse +import json +from pathlib import Path +from generate_update_manifest import encode_manifest, upcoming_notes + + +def refresh(manifest, readme): + if manifest.get("schemaVersion") != 1 or manifest.get("packageName") != "cx.aswin.boxlore" or manifest.get("testOnly"): + raise ValueError("Expected a shipping update manifest") + result = dict(manifest, upcoming=upcoming_notes(readme)) + if len(encode_manifest(result)) > 64 * 1024: + raise ValueError("Update feed exceeds 64 KiB; shorten Upcoming Changes") + return result + + +if __name__ == "__main__": + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--manifest", type=Path, required=True) + parser.add_argument("--readme", type=Path, default=Path("README.md")) + args = parser.parse_args() + args.manifest.write_bytes(encode_manifest(refresh(json.loads(args.manifest.read_text()), args.readme.read_text()))) diff --git a/.github/scripts/rehearse_release.py b/.github/scripts/rehearse_release.py new file mode 100644 index 000000000..2d855e510 --- /dev/null +++ b/.github/scripts/rehearse_release.py @@ -0,0 +1,89 @@ +"""Exercise release-note preparation offline, writing only preview artifacts. + +This is deliberately independent of publishing, FCM and repository credentials. +It exercises local reviewed copy; it cannot discover unrecorded merged PRs. +""" + +import argparse +import json +from datetime import datetime, timezone +from pathlib import Path + +import manual_dispatch +import prepare_release as release +import update_changelog as copy + + +def candidate(gradle, changelog, readme, *, operation, bump, release_date): + current = release.read_app_version(gradle) + if operation == "refresh-latest-artifacts": + return { + "report": { + "operation": operation, "current_version": current.name, + "candidate_version": current.name, "candidate_code": current.code, + "version_bumped": False, "announcement": None, + "guidance": "A same-version refresh stays quiet. No release alert is prepared.", + }, + "gradle": gradle, "changelog": changelog, "readme": readme, + } + if operation != "prepare-release": + raise ValueError("Rehearsal supports prepare-release or refresh-latest-artifacts") + # Real copy ordering and promotion functions; only in-memory copies are changed. + groups = copy.reviewed_readme_groups(changelog) + reviewed_readme = copy._update_readme(readme, groups=groups) + target = release.bump_version(current, bump) + promoted_changelog = release.promote_changelog(changelog, target, release_date) + promoted_readme = release.promote_readme(reviewed_readme, target, release_date) + message = manual_dispatch.argument_parser().parse_args([ + "--title", f"boxlore {target.tag} is here", + "--body", release.notification_body(promoted_readme, target), + "--type", "both", "--target", "debug_users", "--test-mode", "true", + "--preview-only", "true", "--action-label", "Download", + "--route", "https://example.invalid/rehearsal.apk", + ]) + plan = manual_dispatch.notification_plan(message) + return { + "report": { + "operation": operation, "current_version": current.name, + "candidate_version": target.name, "candidate_code": target.code, + "version_bumped": True, "announcement": {**plan, "mode": "offline-preview", "sent": False}, + "guidance": "Review the candidate notes and message. No version files, release or notifications were changed.", + }, + "gradle": release.replace_gradle_version(gradle, current, target), + "changelog": promoted_changelog, "readme": promoted_readme, + } + + +def write_preview(root, output, *, operation="prepare-release", bump="patch", release_date=None): + root, output = Path(root).resolve(), Path(output).resolve() + # Keep artifacts away from the three source-file locations, including symlinks. + destinations = [output / name for name in ("report.json", "candidate.gradle.kts", "candidate-changelog.md", "candidate-readme.md")] + sources = [root / name for name in ("app/build.gradle.kts", "CHANGELOG.md", "README.md")] + if any(dest.resolve() in {source.resolve() for source in sources} for dest in destinations): + raise ValueError("Choose a separate preview directory; source files must stay unchanged") + preview = candidate( + *(source.read_text(encoding="utf-8") for source in sources), + operation=operation, bump=bump, + release_date=release_date or datetime.now(timezone.utc).date().isoformat(), + ) + output.mkdir(parents=True, exist_ok=True) + for path, content in zip(destinations, ( + json.dumps(preview["report"], ensure_ascii=False, indent=2) + "\n", + preview["gradle"], preview["changelog"], preview["readme"], + )): + path.write_text(content, encoding="utf-8") + return preview["report"] + + +if __name__ == "__main__": + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--root", default=".") + parser.add_argument("--output", required=True) + parser.add_argument("--operation", choices=("prepare-release", "refresh-latest-artifacts"), default="prepare-release") + parser.add_argument("--bump", choices=("patch", "minor", "major"), default="patch") + args = parser.parse_args() + try: + report = write_preview(args.root, args.output, operation=args.operation, bump=args.bump) + print(report["guidance"]) + except (ValueError, OSError) as error: + parser.exit(1, f"Rehearsal stopped: {error}\nFix the indicated copy or file, then rerun. Nothing was published or sent.\n") diff --git a/.github/scripts/release_classfile.py b/.github/scripts/release_classfile.py index 0639b7906..2d9abc8f5 100644 --- a/.github/scripts/release_classfile.py +++ b/.github/scripts/release_classfile.py @@ -112,7 +112,7 @@ def members() -> list[Member]: return ClassFile(name, super_name, access, fields, methods, signature(attrs)) -def load_project_classes(root: Path) -> dict[str, ClassFile]: +def load_project_classes(root: Path, app_variant: str = "release") -> dict[str, ClassFile]: result: dict[str, ClassFile] = {} modules = [root / "app", *sorted((root / "core").glob("*")), *sorted((root / "feature").glob("*"))] for module in modules: @@ -124,11 +124,12 @@ def load_project_classes(root: Path) -> dict[str, ClassFile]: item = parse_class(archive.read(entry)) result[item.name] = item # The application is not published as a library jar. - for directory in [root / "app/build/intermediates/javac/release/compileReleaseJavaWithJavac/classes", - root / "app/build/intermediates/built_in_kotlinc/release/compileReleaseKotlin/classes"]: + task_variant = app_variant[0].upper() + app_variant[1:] + for directory in [root / f"app/build/intermediates/javac/{app_variant}/compile{task_variant}JavaWithJavac/classes", + root / f"app/build/intermediates/built_in_kotlinc/{app_variant}/compile{task_variant}Kotlin/classes"]: for path in sorted(directory.rglob("*.class")): item = parse_class(path.read_bytes()) result[item.name] = item if "cx/aswin/boxlore/core/network/BoxLoreApi" not in result or "cx/aswin/boxlore/LegacyWorkerFactory" not in result: - raise ValueError("Pre-R8 release classes missing; build assembleRelease and bundleRelease first") + raise ValueError(f"Pre-R8 {app_variant} classes missing; build assembleRelease and bundlePlayRelease first") return result diff --git a/.github/scripts/release_verification.init.gradle b/.github/scripts/release_verification.init.gradle index e0c9c2346..62f6fde8c 100644 --- a/.github/scripts/release_verification.init.gradle +++ b/.github/scripts/release_verification.init.gradle @@ -3,5 +3,6 @@ gradle.afterProject { project -> if (project.path == ':app') { project.android.buildTypes.getByName('release').signingConfig = null + project.android.buildTypes.getByName('playRelease').signingConfig = null } } diff --git a/.github/scripts/serve_update_test.py b/.github/scripts/serve_update_test.py new file mode 100644 index 000000000..49f90e7fc --- /dev/null +++ b/.github/scripts/serve_update_test.py @@ -0,0 +1,80 @@ +"""Local-only updater fixture. Never publishes a release or contacts Firebase. + +Install a signed updateTest build first. Build its higher-code successor with +-PboxloreUpdateTestVersionCode=, then serve that APK and reverse adb port 8765. +""" +import argparse +import hashlib +import json +from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer +from pathlib import Path + +DEFAULT_NOTES = 'Isolated update test. No release was published and no alert was sent.' + + +def manifest(apk, code, state='available', notes=DEFAULT_NOTES, upcoming=""): + value = dict(schemaVersion=1, packageName='cx.aswin.boxlore', versionCode=code, + versionName=f'Test {code}', minSdk=99 if state == 'incompatible' else 31, + apkUrl='http://127.0.0.1:8765/candidate.apk', + apkSha256='0' * 64 if state == 'corrupt' else hashlib.sha256(apk.read_bytes()).hexdigest(), + apkBytes=apk.stat().st_size, notesUrl='http://127.0.0.1:8765/notes', + notes=notes, upcoming=upcoming, testOnly=True) + if len(notes.encode('utf-16-le')) // 2 > 24_000: + raise ValueError('Release notes exceed the app limit of 24,000 characters') + if len(json.dumps(value, ensure_ascii=False).encode('utf-8')) > 64 * 1024: + raise ValueError('The update manifest exceeds the app limit of 64 KiB') + return value + + +def handler(apk, code, state, notes=DEFAULT_NOTES, upcoming=""): + class Handler(BaseHTTPRequestHandler): + def do_GET(self): + if self.path == '/update.json': + if state == 'unavailable': + self.send_error(404); return + data = json.dumps(manifest(apk, code, state, notes, upcoming), ensure_ascii=False).encode('utf-8') + self.send_response(200); self.send_header('Content-Type', 'application/json') + self.send_header('Content-Length', str(len(data))); self.end_headers(); self.wfile.write(data) + elif self.path == '/candidate.apk': + size = apk.stat().st_size; start = 0 + if self.headers.get('Range'): + try: start = int(self.headers['Range'].removeprefix('bytes=').removesuffix('-')) + except ValueError: self.send_error(400); return + if start >= size or start < 0: self.send_error(416); return + self.send_response(206 if start else 200) + if start: self.send_header('Content-Range', f'bytes {start}-{size-1}/{size}') + self.send_header('Content-Type', 'application/vnd.android.package-archive') + self.send_header('Content-Length', str(size-start)); self.end_headers() + try: + with apk.open('rb') as stream: + stream.seek(start) + while chunk := stream.read(32768): self.wfile.write(chunk) + except (BrokenPipeError, ConnectionResetError): pass + elif self.path == '/notes': + data = notes.encode('utf-8') + self.send_response(200) + self.send_header('Content-Type', 'text/plain; charset=utf-8') + self.send_header('Content-Length', str(len(data))) + self.end_headers(); self.wfile.write(data) + else: self.send_error(404) + return Handler + + +if __name__ == '__main__': + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument('--apk', type=Path, required=True) + parser.add_argument('--version-code', type=int, required=True) + parser.add_argument('--state', choices=('available','unavailable','incompatible','corrupt'), default='available') + parser.add_argument('--notes-file', type=Path, help='UTF-8 sample release notes for local scrolling tests') + parser.add_argument('--upcoming-file', type=Path, help='UTF-8 local unreleased preview') + args = parser.parse_args() + if not args.apk.is_file() or args.version_code <= 0: parser.error('Use an existing signed test APK and a positive version code') + try: + notes = args.notes_file.read_text(encoding='utf-8') if args.notes_file else DEFAULT_NOTES + upcoming = args.upcoming_file.read_text(encoding='utf-8') if args.upcoming_file else '' + if len(upcoming.encode('utf-16-le')) // 2 > 6_000: raise ValueError('Upcoming preview exceeds 6,000 characters') + manifest(args.apk, args.version_code, args.state, notes, upcoming) + except (OSError, UnicodeError, ValueError) as error: + parser.error(str(error)) + print('Local test feed: http://127.0.0.1:8765/update.json; use adb reverse tcp:8765 tcp:8765', flush=True) + ThreadingHTTPServer(('127.0.0.1', 8765), handler(args.apk, args.version_code, args.state, notes, upcoming)).serve_forever() diff --git a/.github/scripts/sync_announcement_preview.py b/.github/scripts/sync_announcement_preview.py new file mode 100644 index 000000000..e5bdb50c0 --- /dev/null +++ b/.github/scripts/sync_announcement_preview.py @@ -0,0 +1,23 @@ +"""Sync the app font for the browser preview; native Compose and browser markup remain independent.""" +import argparse +from pathlib import Path + +ROOT = Path(__file__).resolve().parents[2] + +def sync(check=False): + sources = {'font.ttf': ROOT / 'core/designsystem/src/main/res/font/google_sans_flex_variable.ttf'} + target = ROOT / 'admin-panel/public/notifyappusers/announcements' + if not check: + target.mkdir(parents=True, exist_ok=True) + for name, source in sources.items(): + destination = target / name + if check: + if not destination.exists() or destination.read_bytes() != source.read_bytes(): + raise ValueError(f'Announcement preview differs: {name}; run sync_announcement_preview.py') + else: + destination.write_bytes(source.read_bytes()) + +if __name__ == '__main__': + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument('--check', action='store_true') + sync(parser.parse_args().check) diff --git a/.github/scripts/test_announcement_admin.cjs b/.github/scripts/test_announcement_admin.cjs new file mode 100644 index 000000000..d744fc777 --- /dev/null +++ b/.github/scripts/test_announcement_admin.cjs @@ -0,0 +1,39 @@ +const test = require('node:test'); +const assert = require('node:assert/strict'); +const {plan, canonical, PALETTES, isPreviewExcluded, parsePreview, reviewRows} = require('../../admin-panel/public/notifyappusers/dashboard.js'); +const base = () => ({title:'boxlore test',body:'First line\nSecond line',type:'both',sound:'silent',action_label:'Open',show_action_in_push:'false',show_action_in_app:'true',category:'ANNOUNCEMENT',presentation:'fullscreen',tone:'primary',image_style:'banner',release_alert:'false',include_play:'false',release_version_code:'0',test_mode:'true',target:'test_users',dry_run:'true',collapse_key:'',route:'',image:''}); +test('isolated audience and legacy payload retained',()=>{const result=plan(base());assert.equal(result.topic,'test_users');assert.equal(result.data.body,'First line\nSecond line');assert.equal(result.data.presentation,'fullscreen');}); +test('test mode rejects production and Play release override is explicit',()=>{for(const target of ['all_users','prod_users','direct_users'])assert.throws(()=>plan({...base(),target}));assert.throws(()=>plan({...base(),test_mode:'false',target:'all_users',release_alert:'true'}));assert.equal(plan({...base(),test_mode:'false',target:'all_users',release_alert:'true',include_play:'true'}).topic,'all_users');}); +test('UTF-8 bounds prevent oversized live request',()=>assert.throws(()=>plan({...base(),body:'é'.repeat(1100)}))); +test('canonical approval preserves exact Unicode text and sorted keys',()=>{assert.equal(JSON.stringify(canonical({b:{z:1,a:2},a:'é'})), '{"a":"é","b":{"a":2,"z":1}}');}); +test('preview classic palettes match actual tokens',()=>{assert.equal(PALETTES.dark.primary[0],'#9C8CFF');assert.equal(PALETTES.light.container,'#EDEDF4');}); + +test('release preview retains GitHub page and rejects other destinations',()=>{const draft={...base(),release_alert:'true',release_url:'https://github.com/boxcreate/boxlore/releases/tag/v29'};assert.equal(plan(draft).data.release_url,draft.release_url);assert.throws(()=>plan({...draft,release_url:'https://evil.example/release'}));}); + +test('Play preview shows no release alert unless explicitly included',()=>{const release={...base(),release_alert:'true'};assert.equal(isPreviewExcluded(release,true),true);assert.equal(isPreviewExcluded(release,false),false);assert.equal(isPreviewExcluded({...release,include_play:'true'},true),false);assert.equal(isPreviewExcluded(base(),true),false);}); + +test('import validates a preview before editing and clears omitted optional destinations',()=>{assert.throws(()=>parsePreview({}));assert.throws(()=>parsePreview({draft:{title:'Broken',body:'Not a preview'}}));const draft=base();const preview=plan(draft);const restored=parsePreview(preview);assert.equal(restored.title,draft.title);assert.equal(restored.route,'');assert.equal(restored.image,'');assert.equal(restored.release_url,'');assert.equal(restored.dry_run,'true');}); + +test('push-only messages do not promise an in-app alert',()=>{assert.equal(isPreviewExcluded({...base(),type:'push'},false),true);assert.equal(isPreviewExcluded({...base(),type:'push'},true),true);assert.equal(isPreviewExcluded({...base(),type:'in-app'},false),false);}); + + +test('push-only review describes notification actions and no in-app buttons',()=>{const draft={...base(),type:'push',release_alert:'true'};const rows=Object.fromEntries(reviewRows(draft,'Isolated test builds'));assert.equal(rows['Appearance'],'Android system notification');assert.equal(rows['In-app actions'],'No in-app alert');assert.equal(rows['Notification action'],'None');assert.equal(rows['Delivery'],'System notification');assert.equal(rows['Send mode'],'Provider validation only — no delivery');}); +test('combined delivery review includes the configured notification action',()=>{const draft={...base(),route:'https://example.org',show_action_in_push:'true'};const rows=Object.fromEntries(reviewRows(draft,'Isolated test builds'));assert.equal(rows['In-app actions'],'Open · Dismiss');assert.equal(rows['Notification action'],'Open');assert.equal(rows['Action destination'],'https://example.org');}); + +test('browser preview formats the native Markdown subset without interpreting HTML',()=>{ + const vm=require('node:vm'),fs=require('node:fs'),path=require('node:path'); + class Node { constructor(tag,text=''){this.tag=tag;this.textContent=text;this.children=[];this.dataset={};} append(...nodes){this.children.push(...nodes);} replaceChildren(){this.children=[];} } + const context=vm.createContext({document:{createElement:tag=>new Node(tag),createTextNode:text=>new Node('#text',text)},window:{addEventListener(){},parent:{postMessage(){}}}}); + vm.runInContext(fs.readFileSync(path.join(__dirname,'../../admin-panel/public/notifyappusers/announcements/renderer.js'),'utf8'),context); + const root=new Node('root'); + context.content(root,'## Heading\nFirst **bold** line\nSecond _italic_ [docs](https://example.org)\n\n1) One\n+ Two\n> A callout\n`code` plain HTML'); + assert.deepEqual(root.children.map(n=>n.tag),['h2','p','div','div','blockquote','p']); + assert.equal(root.children[0].dataset.level,'2'); + assert.equal(root.children[1].children.find(n=>n.tag==='strong').textContent,'bold'); + assert.equal(root.children[1].children.find(n=>n.tag==='em').textContent,'italic'); + const link=root.children[1].children.find(n=>n.tag==='a');assert.equal(link.href,'https://example.org');assert.equal(link.rel,'noopener noreferrer'); + assert.equal(root.children[2].children[0].textContent,'1.');assert.equal(root.children[3].children[0].textContent,'•'); + assert.equal(root.children[5].children.find(n=>n.tag==='code').textContent,'code'); + assert.ok(root.children[5].children.some(n=>n.textContent.includes('plain HTML'))); + context.inline(root,'[bad](javascript:alert(1))');assert.equal(root.children.filter(n=>n.tag==='a').length,0); +}); diff --git a/.github/scripts/test_announcements.py b/.github/scripts/test_announcements.py new file mode 100644 index 000000000..9e99293d8 --- /dev/null +++ b/.github/scripts/test_announcements.py @@ -0,0 +1,140 @@ +import json +import hashlib +import sys +import subprocess +import os +import types +import shlex +import io +import unittest +from pathlib import Path +from unittest.mock import Mock, patch +sys.path.insert(0, str(Path(__file__).resolve().parent)) +import manual_dispatch as sender +import sync_announcement_preview +import serve_update_test +import preview_announcement_on_device + +class AnnouncementContractsTest(unittest.TestCase): + def arguments(self): + return sender.argument_parser().parse_args(['--title','Test','--body','Exact **copy**','--type','in-app','--target','test_users','--test-mode','true']) + def test_browser_preview_uses_the_app_font(self): + sync_announcement_preview.sync(check=True) + def test_real_delivery_requires_approval_for_exact_draft(self): + args=self.arguments(); transport=Mock(return_value='test') + with self.assertRaisesRegex(ValueError,'approve'):sender.send_notification(args,sender=transport) + transport.assert_not_called() + args.approval_digest=sender.plan_digest(sender.notification_plan(args)) + sender.send_notification(args,sender=transport) + args.body='Edited copy' + with self.assertRaises(ValueError):sender.send_notification(args,sender=transport) + self.assertEqual(transport.call_count,1) + def test_test_audience_accepted_and_release_filter_requires_explicit_override(self): + args=self.arguments(); self.assertEqual(sender.notification_plan(args)['topic'],'test_users') + args.test_mode='false';args.target='all_users';args.release_alert='true' + with self.assertRaises(ValueError):sender.notification_plan(args) + args.include_play='true';self.assertEqual(sender.notification_plan(args)['data']['include_play'],'true') + def test_preview_contains_current_digest_and_never_calls_sender(self): + args=self.arguments();args.preview_only='true';transport=Mock() + result=sender.send_notification(args,sender=transport) + transport.assert_not_called();self.assertEqual(result['approval_digest'],sender.plan_digest(sender.notification_plan(args))) + def test_browser_and_workflow_approval_digest_are_identical(self): + args=self.arguments();args.title='boxlore – 更新';args.body='Exact \n **é** copy' + plan=sender.notification_plan(args) + node_script="const {canonical}=require('./admin-panel/public/notifyappusers/dashboard.js');const crypto=require('node:crypto');let data='';process.stdin.on('data',x=>data+=x).on('end',()=>process.stdout.write(crypto.createHash('sha256').update(JSON.stringify(canonical(JSON.parse(data)))).digest('hex')));" + result=subprocess.run(['node','-e',node_script],input=json.dumps(plan,ensure_ascii=False),text=True,capture_output=True,check=True,cwd=Path(__file__).resolve().parents[2]) + self.assertEqual(result.stdout,sender.plan_digest(plan)) + def test_direct_sender_excludes_play_and_test_topics_before_delivery(self): + args=self.arguments();args.target='direct_users';args.test_mode='false' + plan=sender.notification_plan(args) + messaging=Mock();messaging.send.return_value='fixture-id' + firebase=types.ModuleType('firebase_admin');firebase.initialize_app=Mock();firebase.credentials=Mock();firebase.messaging=messaging + with patch.dict(sys.modules,{'firebase_admin':firebase}),patch.dict(os.environ,{'FIREBASE_CREDENTIALS':'{}'}): + sender._firebase_send(plan) + delivered=messaging.Message.call_args.kwargs + self.assertNotIn('topic',delivered) + self.assertEqual("'direct_users' in topics && !('play_users' in topics) && !('test_users' in topics)",delivered['condition']) + def test_ordinary_sender_default_does_not_use_legacy_play_suppressed_release_category(self): + plan=sender.notification_plan(self.arguments()) + self.assertEqual('ANNOUNCEMENT',plan['data']['category']) + self.assertEqual('false',plan['data']['release_alert']) + def test_insecure_image_is_rejected_without_dispatch(self): + args=self.arguments();args.image='http://example.com/image.png' + with self.assertRaisesRegex(ValueError,'HTTPS'):sender.notification_plan(args) + def test_local_device_preview_uses_only_adb_and_requires_receiver_success(self): + import tempfile + with tempfile.TemporaryDirectory() as directory: + data={'title':'Local test','body':"Listener's `copy` with $(symbols) and Unicode 更新"} + path=Path(directory)/'preview.json';path.write_text(json.dumps({'data':data})) + runner=Mock(return_value=subprocess.CompletedProcess([],0,'Broadcast completed: result=-1','')) + preview_announcement_on_device.preview(path,runner=runner) + command=runner.call_args.args[0] + remote=shlex.split(command[-1]) + self.assertEqual('adb',command[0]);self.assertEqual('shell',command[-2]) + self.assertIn('cx.aswin.boxlore/.testing.AnnouncementTestReceiver',remote) + self.assertEqual(data,json.loads(remote[-1])) + runner.return_value=subprocess.CompletedProcess([],0,'Broadcast completed: result=0','') + with self.assertRaisesRegex(ValueError,'isolated'):preview_announcement_on_device.preview(path,runner=runner) + def test_loopback_fixture_is_explicitly_marked_test_only(self): + import tempfile + with tempfile.TemporaryDirectory() as directory: + path=Path(directory)/'test.apk';path.write_bytes(b'fixture') + value=serve_update_test.manifest(path,29) + self.assertTrue(value['testOnly']);self.assertEqual(value['apkUrl'],'http://127.0.0.1:8765/candidate.apk') + def test_local_update_fixture_serves_exact_bytes_and_resumes_a_partial_download(self): + import tempfile + class Connection: + def __init__(self,request): self.request=request;self.response=bytearray() + def makefile(self,*args): return io.BytesIO(self.request) + def sendall(self,data): self.response.extend(data) + with tempfile.TemporaryDirectory() as directory: + path=Path(directory)/'test.apk';path.write_bytes(b'approved-candidate-bytes') + Handler=serve_update_test.handler(path,29,'available') + Handler.log_message=lambda *args:None + for headers,status,expected in [(b'',b'200',path.read_bytes()),(b'Range: bytes=9-\r\n',b'206',path.read_bytes()[9:])]: + connection=Connection(b'GET /candidate.apk HTTP/1.0\r\n'+headers+b'\r\n') + Handler(connection,('127.0.0.1',0),Mock()) + header,body=bytes(connection.response).split(b'\r\n\r\n',1) + self.assertIn(status,header.split(b'\r\n')[0]);self.assertEqual(expected,body) + def test_long_local_notes_are_preserved_in_manifest_and_browser_view(self): + import tempfile + class Connection: + def __init__(self, route): self.route=route;self.response=bytearray() + def makefile(self,*args): return io.BytesIO(f'GET {self.route} HTTP/1.0\r\n\r\n'.encode()) + def sendall(self,data): self.response.extend(data) + with tempfile.TemporaryDirectory() as directory: + path=Path(directory)/'test.apk';path.write_bytes(b'candidate') + notes='Sample notes — scrolling test.\n'*500+'End of test release notes.' + Handler=serve_update_test.handler(path,29,'available',notes);Handler.log_message=lambda *args:None + for route in ('/update.json','/notes'): + connection=Connection(route);Handler(connection,('127.0.0.1',0),Mock()) + header,body=bytes(connection.response).split(b'\r\n\r\n',1) + self.assertIn(b'200',header.split(b'\r\n')[0]) + self.assertEqual(notes,json.loads(body)['notes'] if route=='/update.json' else body.decode('utf-8')) + def test_local_notes_respect_android_character_and_utf8_response_limits(self): + import tempfile + with tempfile.TemporaryDirectory() as directory: + path=Path(directory)/'test.apk';path.write_bytes(b'candidate') + for notes in ('a'*24_001,'🎧'*12_001): + with self.assertRaisesRegex(ValueError,'24,000'):serve_update_test.manifest(path,29,notes=notes) + with self.assertRaisesRegex(ValueError,'64 KiB'):serve_update_test.manifest(path,29,notes='界'*23_000) + def test_local_update_fixture_exposes_error_incompatible_and_integrity_test_states(self): + import tempfile + class Connection: + def __init__(self): self.response=bytearray() + def makefile(self,*args): return io.BytesIO(b'GET /update.json HTTP/1.0\r\n\r\n') + def sendall(self,data): self.response.extend(data) + with tempfile.TemporaryDirectory() as directory: + path=Path(directory)/'test.apk';path.write_bytes(b'candidate') + for state in ('unavailable','incompatible','corrupt'): + Handler=serve_update_test.handler(path,29,state);Handler.log_message=lambda *args:None + connection=Connection();Handler(connection,('127.0.0.1',0),Mock()) + header,body=bytes(connection.response).split(b'\r\n\r\n',1) + if state=='unavailable':self.assertIn(b'404',header.split(b'\r\n')[0]) + elif state=='incompatible':self.assertEqual(99,json.loads(body)['minSdk']) + else:self.assertNotEqual(hashlib.sha256(path.read_bytes()).hexdigest(),json.loads(body)['apkSha256']) + def test_release_publication_only_previews_and_play_bundle_remains_separate(self): + workflow=(Path(__file__).resolve().parents[2]/'.github/workflows/changelog-on-merge.yml').read_text() + announce=workflow.split(' announce-release:')[1] + self.assertIn("preview_only: 'true'",announce);self.assertIn('target: direct_users',announce) + self.assertIn('bundlePlayRelease',workflow) diff --git a/.github/scripts/test_manual_dispatch.py b/.github/scripts/test_manual_dispatch.py new file mode 100644 index 000000000..ffa742307 --- /dev/null +++ b/.github/scripts/test_manual_dispatch.py @@ -0,0 +1,98 @@ +"""Hermetic sender tests: no Firebase account and no real messages.""" + +import json +import os +import sys +import tempfile +import unittest +from pathlib import Path +from unittest.mock import Mock, patch + +sys.path.insert(0, str(Path(__file__).resolve().parent)) +import manual_dispatch as sender + + +def args(*extra): + parsed = sender.argument_parser().parse_args([ + "--title", "boxlore test", "--body", "First line\\nSecond line", "--type", "both", *extra, + ]) + if parsed.dry_run == "false" and parsed.preview_only == "false": + parsed.approval_digest = sender.plan_digest(sender.notification_plan(parsed)) + return parsed + + +class ManualDispatchTest(unittest.TestCase): + def test_offline_preview_never_calls_transport(self): + transport = Mock(side_effect=AssertionError("Preview must not send")) + with tempfile.TemporaryDirectory() as temp: + output = Path(temp) / "preview.json" + result = sender.send_notification(args("--preview-only", "true", "--preview-output", str(output)), sender=transport) + self.assertEqual(json.loads(output.read_text())["mode"], "offline-preview") + self.assertFalse(result["sent"]) + transport.assert_not_called() + + def test_test_mode_rejects_both_production_audiences_before_transport(self): + transport = Mock() + for topic in ("all_users", "prod_users"): + with self.subTest(topic=topic), self.assertRaisesRegex(ValueError, "only permits debug_users"): + sender.send_notification(args("--test-mode", "true", "--target", topic), sender=transport) + transport.assert_not_called() + + def test_debug_test_message_and_dry_run_use_exact_transport_settings(self): + for dry_run in ("true", "false"): + with self.subTest(dry_run=dry_run): + transport = Mock(return_value="fake-message-id") + result = sender.send_notification(args("--test-mode", "true", "--target", "debug_users", "--dry-run", dry_run), sender=transport) + self.assertEqual(transport.call_args.args[0]["topic"], "debug_users") + self.assertEqual(transport.call_args.args[0]["dry_run"], dry_run == "true") + self.assertEqual(result["data"]["body"], "First line\nSecond line") + + def test_existing_manual_contract_retains_all_optional_keys(self): + plan = sender.notification_plan(args("--route", "boxlore://podcast/123", "--image", "https://example.invalid/cover.png", "--sound", "silent", "--action-label", "Open show", "--show-action-in-push", "false", "--category", "TIP", "--collapse-key", "manual_tip")) + self.assertEqual(plan["topic"], "all_users") + self.assertEqual(plan["collapse_key"], "manual_tip") + self.assertEqual(plan["data"]["action_label"], "Open show") + self.assertEqual(plan["data"]["show_action_in_push"], "false") + self.assertEqual(plan["data"]["category"], "TIP") + self.assertEqual(plan["data"]["route"], "boxlore://podcast/123") + self.assertIn("image", plan["data"]) + + def test_unicode_size_limit_counts_utf8_keys_and_values(self): + arguments = args() + arguments.body = "é" * 1100 + with self.assertRaisesRegex(ValueError, "Shorten the message"): + sender.notification_plan(arguments) + arguments.body = "x" + base = sender.notification_plan(arguments)["payload_bytes"] - 1 + arguments.body = "x" * (2048 - base) + self.assertEqual(sender.notification_plan(arguments)["payload_bytes"], 2048) + arguments.body += "x" + with self.assertRaises(ValueError): + sender.notification_plan(arguments) + + def test_invalid_controls_fail_closed(self): + for field, value in (("dry_run", "TRUE"), ("test_mode", "yes"), ("show_action_in_push", "maybe"), ("type", "popup"), ("target", "unknown"), ("sound", "loud"), ("title", " ")): + with self.subTest(field=field), self.assertRaises(ValueError): + arguments = args() + setattr(arguments, field, value) + sender.notification_plan(arguments) + + def test_firebase_adapter_preserves_dry_run_and_legacy_payload(self): + # Fake the SDK boundary as well as the higher-level transport: no provider I/O. + firebase = Mock() + credentials, messaging = Mock(), Mock() + firebase.credentials, firebase.messaging = credentials, messaging + messaging.send.return_value = "fake-provider-id" + modules = {"firebase_admin": firebase, "firebase_admin.credentials": credentials, "firebase_admin.messaging": messaging} + arguments = args("--dry-run", "true", "--collapse-key", "test_release") + with patch.dict(sys.modules, modules), patch.dict(os.environ, {"FIREBASE_CREDENTIALS": '{"project_id":"offline-test"}'}): + result = sender.send_notification(arguments) + self.assertEqual(result["response"], "fake-provider-id") + self.assertTrue(messaging.send.call_args.kwargs["dry_run"]) + self.assertEqual(messaging.Message.call_args.kwargs["data"]["type"], "both") + self.assertEqual(messaging.Message.call_args.kwargs["topic"], "all_users") + messaging.AndroidConfig.assert_called_once_with(collapse_key="test_release") + + +if __name__ == "__main__": + unittest.main() diff --git a/.github/scripts/test_release_contracts.py b/.github/scripts/test_release_contracts.py index b4c696139..6c6adc89d 100644 --- a/.github/scripts/test_release_contracts.py +++ b/.github/scripts/test_release_contracts.py @@ -336,13 +336,14 @@ def test_both_publication_paths_and_manual_preflight_are_guarded(self): """Full R8 verification gates publication; preflight is opt-in only.""" root = Path(__file__).resolve().parents[2] workflow = (root / ".github/workflows/changelog-on-merge.yml").read_text() - self.assertEqual(2, workflow.count("python3 .github/scripts/verify_release_contracts.py")) + self.assertEqual(4, workflow.count("python3 .github/scripts/verify_release_contracts.py")) for block in workflow.split("- name: Verify optimized release contracts")[1:]: self.assertIn("app-release.apk", block[:450]) - self.assertIn("app-release.aab", block[:450]) + self.assertIn("app-playRelease.aab", block[:600]) preflight = (root / ".github/workflows/release-contracts.yml").read_text() self.assertIn("app-release-unsigned.apk", preflight) self.assertIn("uploadCrashlyticsMappingFileRelease", preflight) + self.assertIn("--app-variant playRelease", preflight) self.assertIn("workflow_dispatch:", preflight) self.assertNotIn("pull_request:", preflight) self.assertNotIn("push:", preflight) diff --git a/.github/scripts/test_release_copy_validation.py b/.github/scripts/test_release_copy_validation.py new file mode 100644 index 000000000..76a478b15 --- /dev/null +++ b/.github/scripts/test_release_copy_validation.py @@ -0,0 +1,152 @@ +"""Reject missing copy without generating or silently dropping release notes.""" + +import json +import os +import subprocess +import sys +import tempfile +import unittest +from argparse import Namespace +from pathlib import Path +from unittest.mock import patch + +sys.path.insert(0, str(Path(__file__).resolve().parent)) +import prepare_release as pr +import update_changelog as uc +from test_update_changelog_copy import SAMPLE_PR_BODY, EMPTY_COPY_BODY, MINIMAL_CHANGELOG + + +class ReleaseCopyValidationTest(unittest.TestCase): + def test_every_user_impact_requires_both_regions(self): + missing_readme = SAMPLE_PR_BODY.split(uc.PR_README_COPY_START)[0] + for label in uc.USER_IMPACT_LABELS[:-1]: + with self.subTest(label=label), self.assertRaisesRegex(ValueError, "release-copy:readme"): + uc.validate_release_copy("fix: restore downloads", missing_readme, [label]) + + def test_no_user_impact_excludes_listener_copy(self): + impact, _, entries, groups = uc.validate_release_copy("ci: validation", SAMPLE_PR_BODY, ["no-user-impact"]) + self.assertEqual(impact, "no-user-impact") + self.assertTrue(entries) + self.assertEqual(groups, []) + + def test_missing_multiple_or_body_only_labels_are_rejected(self): + for labels in ([], ["backend-change"], ["user-impact-high", "user-impact-low"]): + with self.subTest(labels=labels), self.assertRaisesRegex(ValueError, "exactly one"): + uc.validate_release_copy("fix: user-impact-critical", SAMPLE_PR_BODY, labels) + + def test_placeholders_are_not_authored_copy(self): + with self.assertRaisesRegex(ValueError, "empty/TBD"): + uc.validate_release_copy("fix: example", EMPTY_COPY_BODY, ["user-impact-critical"]) + + def test_duplicate_copy_regions_fail_instead_of_discarding_second_region(self): + with self.assertRaisesRegex(ValueError, "exactly one complete"): + uc.validate_release_copy("fix: example", SAMPLE_PR_BODY + SAMPLE_PR_BODY, ["user-impact-high"]) + + def test_leftover_placeholder_does_not_pass_with_one_valid_note(self): + body = SAMPLE_PR_BODY.replace("### Fixed\n", "### Fixed\n- TBD\n") + with self.assertRaisesRegex(ValueError, "empty/TBD"): + uc.validate_release_copy("fix: example", body, ["user-impact-high"]) + + def test_authored_backslashes_are_literal_not_regex_replacement_codes(self): + readme = "# boxlore\n" + uc._render_release_notes_shell(upcoming_inner="old", whats_new_inner=None) + text = r"The path C:\podcasts\1 now works." + updated = uc._update_readme(readme, groups=[{"heading": "Fixes", "bullets": [text]}]) + self.assertIn(text, updated) + + def test_empty_stored_readme_block_is_still_missing_copy(self): + content = MINIMAL_CHANGELOG.replace("## [0.0.12]", "\n### Fixes\n- TBD\n\n\n## [0.0.12]") + with self.assertRaisesRegex(ValueError, "#900"): + uc.reviewed_readme_groups(content) + + def test_readme_failure_preserves_previous_file(self): + with tempfile.TemporaryDirectory() as temp: + readme = Path(temp) / "README.md" + changelog = Path(temp) / "CHANGELOG.md" + readme.write_text("Existing published README", encoding="utf-8") + changelog.write_text(MINIMAL_CHANGELOG, encoding="utf-8") + with patch.object(uc, "README_PATH", readme), patch.object(uc, "CHANGELOG_PATH", changelog): + with self.assertRaisesRegex(ValueError, "#900.*backfill-changelog"): + uc.sync_readme_upcoming() + self.assertEqual(readme.read_text(), "Existing published README") + + def test_authored_prefixes_and_all_bullets_survive_readme_round_trip(self): + content = uc._upsert_readme_copy_block( + MINIMAL_CHANGELOG, 900, + [{"heading": "Improvements", "bullets": ["Fixed: retain these exact words", "Second authored line"]}], + ) + groups = uc.reviewed_readme_groups(content) + readme = "# boxlore\n" + uc._render_release_notes_shell(upcoming_inner="old", whats_new_inner=None) + updated = uc._update_readme(readme, groups=groups) + self.assertIn("Fixed: retain these exact words", updated) + self.assertIn("Second authored line", updated) + self.assertNotIn("AI-generated summary", updated) + + def test_no_user_impact_block_cannot_leak_into_readme(self): + content = MINIMAL_CHANGELOG.replace("user-impact-low", "no-user-impact") + content = uc._upsert_readme_copy_block(content, 900, [{"heading": "Fixes", "bullets": ["Internal CI only"]}]) + self.assertEqual(uc.reviewed_readme_groups(content), []) + + def test_historical_backfill_supplies_missing_copy_without_rewriting_bullets(self): + with tempfile.TemporaryDirectory() as temp: + path = Path(temp) / "CHANGELOG.md" + path.write_text(MINIMAL_CHANGELOG, encoding="utf-8") + with patch.object(uc, "CHANGELOG_PATH", path): + self.assertTrue(uc.append_changelog(900, "fix: polish", SAMPLE_PR_BODY, ["user-impact-low"])) + updated = path.read_text() + self.assertEqual(uc._extract_unreleased_sections(updated), uc._extract_unreleased_sections(MINIMAL_CHANGELOG)) + self.assertTrue(uc.reviewed_readme_groups(updated)) + + def test_released_pr_backfill_does_not_reinsert_into_upcoming(self): + content = MINIMAL_CHANGELOG.replace("- Older release", "- Older release " + uc._pr_suffix(700)) + with tempfile.TemporaryDirectory() as temp: + path = Path(temp) / "CHANGELOG.md" + path.write_text(content, encoding="utf-8") + with patch.object(uc, "CHANGELOG_PATH", path): + self.assertFalse(uc.append_changelog(700, "fix: old", SAMPLE_PR_BODY, ["user-impact-low"])) + self.assertEqual(content, path.read_text()) + + def test_batch_reconciliation_checks_all_copy_before_writing(self): + pulls = [ + {"number": 971, "title": "fix: first", "body": SAMPLE_PR_BODY, "labels": [{"name": "user-impact-critical"}]}, + {"number": 972, "title": "fix: second", "body": EMPTY_COPY_BODY, "labels": [{"name": "user-impact-critical"}]}, + ] + with tempfile.TemporaryDirectory() as temp: + path = Path(temp) / "CHANGELOG.md" + path.write_text(MINIMAL_CHANGELOG, encoding="utf-8") + with patch.object(pr, "CHANGELOG_PATH", path), patch.object(uc, "CHANGELOG_PATH", path), patch.object(pr, "pull_requests_between", return_value=pulls): + with self.assertRaisesRegex(ValueError, "release-copy:changelog"): + pr.reconcile_changelog("boxcreate/boxlore", "unused", "v0.0.12", "candidate") + self.assertEqual(MINIMAL_CHANGELOG, path.read_text()) + + def test_prepare_copy_failure_keeps_version_and_download_links_unchanged(self): + with tempfile.TemporaryDirectory() as temp: + root = Path(temp) + gradle = root / "build.gradle.kts" + changelog = root / "CHANGELOG.md" + readme = root / "README.md" + gradle.write_text('versionCode = 12\nversionName = "0.0.12"\n') + changelog.write_text(MINIMAL_CHANGELOG.replace("[0.0.12]", "[v0.0.12]")) + readme.write_text(uc._render_release_notes_shell( + upcoming_inner="Reviewed notes", + whats_new_inner=uc._render_whats_new_inner("v0.0.12", "2026-07-25", "Published notes"), + )) + paths = [gradle, changelog, readme] + original = [path.read_bytes() for path in paths] + pulls = [{"number": 971, "title": "fix: example", "body": EMPTY_COPY_BODY, "labels": [{"name": "user-impact-high"}]}] + with patch.object(pr, "APP_GRADLE_PATH", gradle), patch.object(pr, "CHANGELOG_PATH", changelog), patch.object(pr, "README_PATH", readme), patch.object(uc, "CHANGELOG_PATH", changelog), patch.object(uc, "README_PATH", readme), patch.object(pr, "assert_remote_target_is_free"), patch.object(pr, "pull_requests_between", return_value=pulls), patch.dict(os.environ, {"GITHUB_REPOSITORY": "boxcreate/boxlore", "GITHUB_TOKEN": "offline-test"}, clear=True): + with self.assertRaisesRegex(ValueError, "release-copy:changelog"): + pr.prepare_release(Namespace(bump="patch", latest_tag="v0.0.12", head_sha="candidate", skip_notify=False, use_readme_upcoming=False)) + self.assertEqual(original, [path.read_bytes() for path in paths]) + + def test_cli_validates_event_without_api_key_or_file_mutation(self): + with tempfile.TemporaryDirectory() as temp: + event = Path(temp) / "event.json" + event.write_text(json.dumps({"pull_request": {"title": "fix: downloads", "body": SAMPLE_PR_BODY, "labels": [{"name": "user-impact-high"}]}})) + env = {"PATH": os.environ["PATH"], "GITHUB_EVENT_PATH": str(event)} + result = subprocess.run([sys.executable, str(Path(uc.__file__).resolve()), "validate-copy"], cwd=temp, env=env, capture_output=True, text=True) + self.assertEqual(result.returncode, 0, result.stderr) + self.assertEqual(list(Path(temp).iterdir()), [event]) + + +if __name__ == "__main__": + unittest.main() diff --git a/.github/scripts/test_release_rehearsal.py b/.github/scripts/test_release_rehearsal.py new file mode 100644 index 000000000..e5d86471a --- /dev/null +++ b/.github/scripts/test_release_rehearsal.py @@ -0,0 +1,93 @@ +"""Offline rehearsal must leave source files, releases and audiences untouched.""" + +import hashlib +import json +import sys +import tempfile +import unittest +from pathlib import Path + +sys.path.insert(0, str(Path(__file__).resolve().parent)) +import rehearse_release as rehearsal +import update_changelog as copy +from test_update_changelog_copy import MINIMAL_CHANGELOG + + +GRADLE = 'android { defaultConfig {\nversionCode = 12\nversionName = "0.0.12"\n} }\n' +README = "# boxlore\n" + copy._render_release_notes_shell(upcoming_inner="old", whats_new_inner=None) +CHANGELOG = copy._upsert_readme_copy_block( + MINIMAL_CHANGELOG, 900, + [{"heading": "Improvements", "bullets": ["Your library opens faster.", "Exact second note."]}], +) + + +class ReleaseRehearsalTest(unittest.TestCase): + def test_prepare_uses_real_copy_and_version_functions(self): + preview = rehearsal.candidate(GRADLE, CHANGELOG, README, operation="prepare-release", bump="minor", release_date="2026-10-10") + self.assertIn('versionName = "0.1.0"', preview["gradle"]) + self.assertIn("versionCode = 13", preview["gradle"]) + self.assertIn("## [v0.1.0] - 2026-10-10", preview["changelog"]) + self.assertIn("Your library opens faster.", preview["readme"]) + plan = preview["report"]["announcement"] + self.assertEqual(plan["topic"], "debug_users") + self.assertEqual(plan["mode"], "offline-preview") + self.assertFalse(plan["sent"]) + + def test_quiet_refresh_keeps_version_and_does_not_prepare_notification(self): + preview = rehearsal.candidate(GRADLE, CHANGELOG, README, operation="refresh-latest-artifacts", bump="major", release_date="2026-10-10") + self.assertEqual(preview["gradle"], GRADLE) + self.assertEqual(preview["changelog"], CHANGELOG) + self.assertEqual(preview["readme"], README) + self.assertFalse(preview["report"]["version_bumped"]) + self.assertIsNone(preview["report"]["announcement"]) + + def sources(self, root, changelog=CHANGELOG): + (root / "app").mkdir() + values = {"app/build.gradle.kts": GRADLE, "CHANGELOG.md": changelog, "README.md": README} + for name, value in values.items(): + (root / name).write_text(value, encoding="utf-8") + return {name: hashlib.sha256((root / name).read_bytes()).hexdigest() for name in values} + + def test_report_writing_never_changes_source_files(self): + with tempfile.TemporaryDirectory() as temp: + root = Path(temp) + original = self.sources(root) + output = root / "preview" + rehearsal.write_preview(root, output, release_date="2026-10-10") + self.assertEqual(original, {name: hashlib.sha256((root / name).read_bytes()).hexdigest() for name in original}) + report = json.loads((output / "report.json").read_text()) + self.assertEqual(report["candidate_code"], 13) + self.assertEqual(len(list(output.iterdir())), 4) + + def test_missing_copy_fails_before_any_artifact_is_written(self): + with tempfile.TemporaryDirectory() as temp: + root = Path(temp) + original = self.sources(root, MINIMAL_CHANGELOG) + with self.assertRaisesRegex(ValueError, "#900"): + rehearsal.write_preview(root, root / "preview") + self.assertFalse((root / "preview").exists()) + self.assertEqual(original, {name: hashlib.sha256((root / name).read_bytes()).hexdigest() for name in original}) + + def test_output_symlink_cannot_overwrite_source(self): + with tempfile.TemporaryDirectory() as temp: + root = Path(temp) + self.sources(root) + output = root / "preview" + output.mkdir() + (output / "candidate.gradle.kts").symlink_to(root / "app/build.gradle.kts") + with self.assertRaisesRegex(ValueError, "source files"): + rehearsal.write_preview(root, output) + self.assertEqual((root / "app/build.gradle.kts").read_text(), GRADLE) + self.assertFalse((output / "report.json").exists()) + + def test_rehearsal_workflow_has_no_publish_or_send_capability(self): + workflows = Path(__file__).resolve().parent.parent / "workflows" + source = (workflows / "release-rehearsal.yml").read_text() + self.assertIn("workflow_dispatch:", source) + self.assertIn("contents: read", source) + for forbidden in ("secrets.", "contents: write", "gh release", "git push", "send-notification.yml", "FIREBASE_CREDENTIALS"): + self.assertNotIn(forbidden, source) + + +if __name__ == "__main__": + unittest.main() diff --git a/.github/scripts/test_update_changelog_copy.py b/.github/scripts/test_update_changelog_copy.py index b4d27eb59..3a173aaa8 100644 --- a/.github/scripts/test_update_changelog_copy.py +++ b/.github/scripts/test_update_changelog_copy.py @@ -18,7 +18,7 @@ SAMPLE_PR_BODY = """ ## Summary -Something Groq would rewrite into different words. +Context that must not replace the exact release wording. ### Added @@ -108,7 +108,7 @@ def test_normalize_readme_heading_aliases(self) -> None: class LockedChangelogFlowTest(unittest.TestCase): - def test_append_uses_verbatim_copy_and_skips_groq(self) -> None: + def test_append_uses_verbatim_copy(self) -> None: updated, changed = uc._update_changelog( MINIMAL_CHANGELOG, uc.parse_pr_changelog_copy(SAMPLE_PR_BODY), @@ -132,29 +132,22 @@ def test_append_uses_verbatim_copy_and_skips_groq(self) -> None: self.assertNotIn("Missing episodes from a show", " ".join(parsed.get("Fixed", []))) self.assertTrue(any("PodcastRepository" in b for b in parsed["Added"])) - def test_sync_changelog_does_not_call_groq_when_all_locked(self) -> None: - locked, _ = uc._update_changelog( - MINIMAL_CHANGELOG, - {"Fixed": ["Author-written fix bullet"]}, - 971, - impact="user-impact-critical", - copy_locked=True, + def test_sync_preserves_every_locked_and_legacy_bullet(self) -> None: + content, _ = uc._update_changelog( + MINIMAL_CHANGELOG, {"Changed": ["Exact first line", "Exact second line"]}, + 971, impact="user-impact-critical", copy_locked=True, ) - with patch.object(uc, "_groq_curate_changelog_unreleased") as groq: - locked_sections, unlocked = uc._partition_locked_sections( - uc._extract_unreleased_sections(locked) - ) - self.assertTrue(any(locked_sections.values())) - self.assertIn("Author-written fix bullet", locked_sections["Fixed"][0]) - self.assertTrue(uc._is_copy_locked(locked_sections["Fixed"][0])) - curated = uc._merge_locked_and_curated( - locked_sections, - {"Fixed": ["Groq would rewrite this ([#971](https://github.com/boxcreate/boxlore/pull/971))"]}, - ) - self.assertIn("Author-written fix bullet", curated["Fixed"][0]) - self.assertTrue(uc._is_copy_locked(curated["Fixed"][0])) - self.assertFalse(any("Groq would rewrite" in b for b in curated["Fixed"])) - groq.assert_not_called() + with tempfile.TemporaryDirectory() as tmp: + path = Path(tmp) / "CHANGELOG.md" + path.write_text(content, encoding="utf-8") + with patch.object(uc, "CHANGELOG_PATH", path): + uc.sync_changelog_unreleased() + updated = path.read_text(encoding="utf-8") + before = uc._extract_unreleased_sections(content) + after = uc._extract_unreleased_sections(updated) + self.assertCountEqual(before["Changed"], after["Changed"]) + self.assertLess(updated.index("Exact first line"), updated.index("Unrelated polish")) + self.assertFalse(uc.sync_changelog_unreleased()) def test_readme_sync_uses_locked_copy_verbatim_without_ai_notice(self) -> None: changelog, _ = uc._update_changelog( @@ -261,33 +254,32 @@ def test_promote_readme_preserves_verbatim_upcoming_without_ai_notice(self) -> N self.assertIn("Missing episodes now show up in the same show.", whats_new) self.assertNotIn("AI-generated summary", whats_new) - def test_append_changelog_skips_groq_when_copy_present(self) -> None: + def test_append_stores_authored_copy_without_credentials(self) -> None: with tempfile.TemporaryDirectory() as tmp: path = Path(tmp) / "CHANGELOG.md" path.write_text(MINIMAL_CHANGELOG, encoding="utf-8") - with patch.object(uc, "CHANGELOG_PATH", path), patch.object( - uc, "_groq_entries" - ) as groq: + with patch.object(uc, "CHANGELOG_PATH", path): changed = uc.append_changelog( - "unused-key", - 971, - "feat(catalog): fill missing PI episodes", - SAMPLE_PR_BODY, + 971, "feat(catalog): fill missing PI episodes", SAMPLE_PR_BODY, labels=["user-impact-critical", "backend-change"], ) - groq.assert_not_called() self.assertTrue(changed) text = path.read_text(encoding="utf-8") self.assertIn("copy:locked", text) self.assertIn("readme-copy:start pr=971", text) self.assertIn("PodcastRepository unions publisher-feed extras", text) self.assertIn("Missing episodes from a show", text) + self.assertFalse(uc.append_changelog(971, "same", SAMPLE_PR_BODY, labels=[])) + self.assertEqual(text, path.read_text(encoding="utf-8")) - def test_groq_is_not_called_when_appending_locked_copy(self) -> None: - with patch.object(uc, "_groq_entries") as groq: - entries = uc.parse_pr_changelog_copy(SAMPLE_PR_BODY) - self.assertTrue(entries) - groq.assert_not_called() + def test_missing_copy_fails_before_changelog_write(self) -> None: + with tempfile.TemporaryDirectory() as tmp: + path = Path(tmp) / "CHANGELOG.md" + path.write_text(MINIMAL_CHANGELOG, encoding="utf-8") + with patch.object(uc, "CHANGELOG_PATH", path): + with self.assertRaisesRegex(ValueError, "release-copy:changelog"): + uc.append_changelog(971, "fix: example", EMPTY_COPY_BODY, ["user-impact-critical"]) + self.assertEqual(MINIMAL_CHANGELOG, path.read_text(encoding="utf-8")) class ChangelogSanitizationAndExtractionTest(unittest.TestCase): diff --git a/.github/scripts/test_update_manifest.py b/.github/scripts/test_update_manifest.py new file mode 100644 index 000000000..538e9c37c --- /dev/null +++ b/.github/scripts/test_update_manifest.py @@ -0,0 +1,74 @@ +import hashlib +from pathlib import Path +import tempfile +import unittest + +from refresh_upcoming import refresh +from generate_update_manifest import build_manifest, listener_notes, upcoming_notes + + +class UpdateManifestTests(unittest.TestCase): + def test_manifest_uses_exact_candidate_version_immutable_url_and_checksum(self): + with tempfile.TemporaryDirectory() as tmp: + apk = Path(tmp) / "candidate.apk" + apk.write_bytes(b"verified signed fixture") + manifest = build_manifest('versionCode = 29\nversionName = "0.0.29"\nminSdk = 31', apk, "boxlore-v0.0.29.apk", "Improved playback") + self.assertEqual(29, manifest["versionCode"]) + self.assertEqual("0.0.29", manifest["versionName"]) + self.assertEqual(hashlib.sha256(apk.read_bytes()).hexdigest(), manifest["apkSha256"]) + self.assertEqual(apk.stat().st_size, manifest["apkBytes"]) + self.assertEqual("https://github.com/boxcreate/boxlore/releases/download/v0.0.29/boxlore-v0.0.29.apk", manifest["apkUrl"]) + self.assertEqual("Improved playback", manifest["notes"]) + + def test_listener_bullets_preserved_without_notification_truncation(self): + first = "Long authored copy " + "a" * 200 + readme = '
  • ' + first + '
  • Fixed: Playback & queues.
' + self.assertEqual("- " + first + "\n- Fixed: Playback & queues.", listener_notes(readme)) + + def test_listener_markdown_preserves_authored_emphasis_and_links(self): + readme = '' + self.assertEqual('- **Better playback** with [details](https://example.com).', listener_notes(readme)) + + def test_upcoming_preserves_authored_copy(self): + readme = 'Improvements:
  • Library: clearer controls.
' + self.assertEqual('## Improvements\n- **Library:** clearer controls.', upcoming_notes(readme)) + self.assertEqual('', upcoming_notes('No upcoming region')) + + def test_preview_refresh_never_changes_binary_metadata(self): + original = dict(schemaVersion=1, packageName='cx.aswin.boxlore', versionCode=29, apkSha256='verified') + changed = refresh(original, '
  • Clearer controls
') + self.assertEqual(original, {k: v for k, v in changed.items() if k != 'upcoming'}) + self.assertEqual('- Clearer controls', changed['upcoming']) + with self.assertRaises(ValueError): refresh(dict(original, testOnly=True), '') + + def test_invalid_asset_and_empty_apk_rejected(self): + with tempfile.TemporaryDirectory() as tmp: + apk = Path(tmp) / "candidate.apk" + apk.write_bytes(b"a") + gradle = 'versionCode = 29\nversionName = "0.0.29"\nminSdk = 31' + for asset in ("../other.apk", "a.aab", "a.apk?x=1"): + with self.assertRaises(ValueError): build_manifest(gradle, apk, asset) + apk.write_bytes(b"") + with self.assertRaises(ValueError): build_manifest(gradle, apk, "a.apk") + + def test_multibyte_notes_cannot_exceed_client_response_limit(self): + with tempfile.TemporaryDirectory() as tmp: + apk = Path(tmp) / "candidate.apk" + apk.write_bytes(b"fixture") + with self.assertRaisesRegex(ValueError, "64 KiB"): + build_manifest('versionCode = 29\nversionName = "0.0.29"\nminSdk = 31', apk, "a.apk", "🎧" * 20_000) + + def test_workflow_publishes_manifest_only_after_download_verification(self): + root = Path(__file__).resolve().parents[2] + workflow = (root / ".github/workflows/changelog-on-merge.yml").read_text() + jobs = workflow.split(" - name: Verify README latest-download APK")[1:] + self.assertEqual(2, len(jobs)) + for job in jobs: + self.assertLess(job.index("exit 1"), job.index("- name: Publish verified update manifest")) + self.assertIn("generate_update_manifest.py", job) + self.assertIn('gh release upload "$TAG" "$RUNNER_TEMP/update.json"', job) + self.assertIn(":app:bundlePlayRelease", workflow) + self.assertNotIn("bundleRelease", workflow) + + +if __name__ == "__main__": unittest.main() diff --git a/.github/scripts/update_changelog.py b/.github/scripts/update_changelog.py index 938753f56..20f32e28a 100644 --- a/.github/scripts/update_changelog.py +++ b/.github/scripts/update_changelog.py @@ -3,7 +3,7 @@ Author-written copy in the PR body (`` and ``) is highest priority: it is pasted verbatim, -tagged ``, and never sent to Groq for rewrite. prepare_release +tagged ``, and ordered without rewriting. prepare_release promotes locked README Upcoming into What's New as-is. """ @@ -14,9 +14,6 @@ import os import re import sys -import time -import urllib.error -import urllib.request from collections import defaultdict from dataclasses import dataclass, field from pathlib import Path @@ -47,9 +44,6 @@ "" "

" ) -GROQ_API_URL = "https://api.groq.com/openai/v1/chat/completions" -GROQ_MODEL = os.environ.get("GROQ_MODEL", "llama-3.3-70b-versatile") -GROQ_USER_AGENT = "boxlore-changelog/1.4" CATEGORY_ORDER = ("Added", "Changed", "Fixed", "Deprecated", "Removed", "Security") README_GROUP_ORDER = ( "Critical", @@ -468,91 +462,6 @@ def _clean_pr_body(body: str) -> str: return cleaned.strip() -def _groq_entries(api_key: str, pr_number: int, pr_title: str, pr_body: str) -> dict[str, list[str]]: - system_prompt = """You write Keep a Changelog entries for boxlore (Android/Kotlin podcast app). -Return ONLY valid JSON with optional keys: Added, Changed, Fixed, Deprecated, Removed, Security. -Each value is an array of bullet strings WITHOUT leading dashes. - -Audience: developers and technical contributors reading CHANGELOG.md (NOT end-user README copy). - -Rules: -- Use precise technical wording: class/module names, tiers, repositories, and behavior when relevant. -- Merge related changes into one bullet per feature area (e.g. all SmartQueueEngine work → one Added bullet). -- For CI-only, dependency-bump, or docs-only PRs, still return exactly one short Changed (or docs) bullet — never an empty object. -- Prefer summarizing analytics as "PostHog announcement viewed/dismissed/action events" — do not list every event property. -- When the PR body includes "What changes in the user's life" / Listener impact, still write technical CHANGELOG bullets, but do not invent UI toolkit branding (e.g. "Material 3") as the main story. -- Use "Fixed ..." for bugs, "Added ..." for features, "Changed ..." for behavior/refactors, "Removed ..." for deletions. -- Do not invent changes unsupported by the PR title/body. -- Omit empty categories. -- No version headers, dates, PR numbers, or markdown in bullets. -- Aim for 1–6 bullets total per PR (at least one). - -Examples: -- Bugfix PR: - Input: Title: fix(queue): restore same-show playback continuation (#1017) - Output: {"Fixed": ["Fixed same-show continuation in SmartQueueEngine by restoring forward chronological queries in LocalEpisodeCatalogDao and RssEpisodeDao"]} -- Feature PR: - Input: Title: feat(info): add direct publisher feed refresh on pull-down (#1016) - Output: {"Added": ["Added direct-feed refresh for subscribed catalog podcasts on pull-to-refresh", "Preserved notification and download settings across pull-to-refresh"]}""" - - cleaned_body = _clean_pr_body(pr_body) - user_prompt = f"""PR #{pr_number} -Title: {pr_title} - -Description: -{cleaned_body or "(no description)"} - -Generate changelog bullets for the [Unreleased] section.""" - - parsed = _groq_chat_json( - api_key, - system_prompt, - user_prompt, - "Groq API error", - ) - if not parsed: - return {} - - normalized: dict[str, list[str]] = {} - for category in CATEGORY_ORDER: - raw = parsed.get(category, []) - if not isinstance(raw, list): - continue - bullets = [str(item).strip() for item in raw if str(item).strip()] - if bullets: - normalized[category] = bullets - return normalized - - -def _fallback_entries_from_title(pr_title: str) -> dict[str, list[str]]: - """Deterministic changelog bullets when Groq returns nothing (e.g. CI/deps PRs).""" - title = (pr_title or "").strip() or "Maintenance update" - conventional = re.match( - r"^(?Pfeat|fix|chore|ci|docs|refactor|test|perf|build|style)" - r"(?:\([^)]*\))?!?:\s*(?P.+)$", - title, - flags=re.IGNORECASE, - ) - if conventional: - kind = conventional.group("type").lower() - summary = conventional.group("summary").strip() - else: - kind = "chore" - summary = title - - if kind == "feat": - category = "Added" - elif kind == "fix": - category = "Fixed" - elif kind == "docs": - category = "Changed" - if not summary.lower().startswith("docs"): - summary = f"Documentation: {summary}" - else: - category = "Changed" - return {category: [summary]} - - def _extract_pr_number(bullet: str) -> int | None: # 1. Full markdown pull request link: [#1019](https://github.com/.../pull/1019) match = re.search(r"\[#(\d+)\]\(https://github\.com/[^/]+/[^/]+/pull/\1\)", bullet) @@ -659,262 +568,10 @@ def _cluster_sections_by_pr(sections: dict[str, list[str]]) -> list[ChangelogClu return clusters -def _render_clustered_changelog(clusters: list[ChangelogCluster]) -> str: - lines: list[str] = [] - for cluster in clusters: - label = f"PR #{cluster.pr_number}" if cluster.pr_number is not None else "Unlinked" - impact = cluster.impact or "unlabeled" - backend = " backend-change" if cluster.backend_change else "" - lines.append( - f"## {label} | impact {impact}{backend} | importance {cluster.importance} | " - f"theme: {cluster.theme}{' | copy-locked' if cluster.copy_locked else ''}" - ) - by_category: dict[str, list[str]] = defaultdict(list) - for category, text in cluster.items: - by_category[category].append(text) - for category in CATEGORY_ORDER: - for text in by_category.get(category, []): - lines.append(f"- [{category}] {text}") - lines.append("") - return "\n".join(lines).strip() - - def _strip_pr_links(text: str) -> str: return re.sub(r"\s*\(\[#\d+\]\([^)]+\)\)\s*$", "", text).strip() -def _groq_retry_wait_seconds(detail: str, attempt: int) -> float: - match = re.search(r"try again in ([0-9]+(?:\.[0-9]+)?)s", detail, flags=re.I) - if match: - return max(1.0, float(match.group(1)) + 0.5) - return min(60.0, float(2 ** attempt)) - - -def _groq_chat_json(api_key: str, system_prompt: str, user_prompt: str, error_label: str) -> dict: - payload = { - "model": GROQ_MODEL, - "temperature": 0.2, - "response_format": {"type": "json_object"}, - "messages": [ - {"role": "system", "content": system_prompt}, - {"role": "user", "content": user_prompt}, - ], - } - - max_attempts = 8 - last_detail = "" - for attempt in range(1, max_attempts + 1): - request = urllib.request.Request( - GROQ_API_URL, - data=json.dumps(payload).encode("utf-8"), - headers={ - "Authorization": f"Bearer {api_key}", - "Content-Type": "application/json", - "User-Agent": GROQ_USER_AGENT, - }, - method="POST", - ) - try: - with urllib.request.urlopen(request, timeout=90) as response: - body = json.loads(response.read().decode("utf-8")) - except urllib.error.HTTPError as exc: - last_detail = exc.read().decode("utf-8", errors="replace") - if exc.code == 429 and attempt < max_attempts: - wait = _groq_retry_wait_seconds(last_detail, attempt) - print( - f"{error_label}: rate limited (attempt {attempt}/{max_attempts}); " - f"retrying in {wait:.1f}s" - ) - time.sleep(wait) - continue - print(f"{error_label} ({exc.code}): {last_detail}", file=sys.stderr) - return {} - except urllib.error.URLError as exc: - last_detail = str(exc) - if attempt < max_attempts: - wait = min(30.0, float(2 ** attempt)) - print( - f"{error_label}: network error (attempt {attempt}/{max_attempts}); " - f"retrying in {wait:.1f}s ({last_detail})" - ) - time.sleep(wait) - continue - print(f"{error_label}: {last_detail}", file=sys.stderr) - return {} - - content = body["choices"][0]["message"]["content"] - parsed = json.loads(content) - if not isinstance(parsed, dict): - print(f"{error_label}: Groq response was not a JSON object", file=sys.stderr) - return {} - return parsed - - print(f"{error_label}: giving up after retries. {last_detail}", file=sys.stderr) - return {} - - -def _format_changelog_bullet( - text: str, - pr_number: int | None, - impact: str | None = None, - backend_change: bool = False, - copy_locked: bool = False, -) -> str: - return _attach_impact( - _format_readme_bullet(text, pr_number), - impact, - backend_change=backend_change, - copy_locked=copy_locked, - ) - - -def _parse_changelog_bullet(entry: object) -> tuple[str, int | None]: - return _parse_readme_bullet(entry) - - -def _sections_from_cluster_bullets( - curated: dict[str, list[tuple[str, int | None]]], - clusters: list[ChangelogCluster], -) -> dict[str, list[str]]: - """Sort bullets within each Keep a Changelog category by cluster importance.""" - pr_importance = {c.pr_number: c.importance for c in clusters if c.pr_number is not None} - pr_impact = {c.pr_number: c.impact for c in clusters if c.pr_number is not None} - pr_backend = { - c.pr_number: c.backend_change for c in clusters if c.pr_number is not None - } - pr_locked = {c.pr_number: c.copy_locked for c in clusters if c.pr_number is not None} - - def sort_key(item: tuple[str, int | None]) -> tuple[int, str]: - _, pr_number = item - return (-pr_importance.get(pr_number, 0), str(pr_number or "")) - - sections: dict[str, list[str]] = {} - for category in CATEGORY_ORDER: - items = curated.get(category, []) - if not items: - continue - sorted_items = sorted(items, key=sort_key) - bullets = [ - _format_changelog_bullet( - text, - pr, - pr_impact.get(pr) if pr is not None else None, - pr_backend.get(pr, False) if pr is not None else False, - copy_locked=pr_locked.get(pr, False) if pr is not None else False, - ) - for text, pr in sorted_items - if text.strip() - ] - if bullets: - sections[category] = bullets - return sections - - -def _fallback_changelog_from_clusters(clusters: list[ChangelogCluster]) -> dict[str, list[str]]: - curated: dict[str, list[tuple[str, int | None]]] = defaultdict(list) - for cluster in clusters: - if not cluster.items: - continue - if cluster.copy_locked: - for category, text in cluster.items: - curated[category].append((text, cluster.pr_number)) - continue - by_category: dict[str, list[str]] = defaultdict(list) - for category, text in cluster.items: - by_category[category].append(text) - for category in CATEGORY_ORDER: - texts = by_category.get(category, []) - if not texts: - continue - merged = texts[0] if len(texts) == 1 else f"{texts[0]} (+ {len(texts) - 1} related changes)" - curated[category].append((merged, cluster.pr_number)) - return _sections_from_cluster_bullets(curated, clusters) - - -def _groq_curate_changelog_unreleased( - api_key: str, sections: dict[str, list[str]] -) -> dict[str, list[str]]: - """Re-group and prioritize the full [Unreleased] section for CHANGELOG.md.""" - if not any(sections.values()): - return {} - - clusters = _cluster_sections_by_pr(sections) - clustered_input = _render_clustered_changelog(clusters) - if not clustered_input: - return {} - - system_prompt = """You curate clustered changelog drafts for boxlore into a clean Keep a Changelog [Unreleased] section. - -Audience: developers — use technical, precise language (class names, tiers, modules, repositories, guards). -This is NOT the user-facing README; do not simplify away useful implementation detail. - -Input is grouped by PR/theme with impact tags and importance scores. Each ## block is one merged feature area. -Impact levels: user-impact-critical (~100), user-impact-high (~95), user-impact-medium (~70), user-impact-low (~45), no-user-impact (~12). -Optional flag backend-change may appear with any impact level (pairable). -Blocks marked copy-locked are author-written and must be preserved verbatim — do not rewrite, merge, or drop them. - -Return ONLY valid JSON: -{"Added": [{"text": "...", "pr": 853}, ...], "Changed": [...], "Fixed": [...], "Removed": [...], ...} - -Rules: -1. ONE bullet object per ## block per category. Merge all [Added] lines in a block into one Added bullet; same for Changed/Fixed/Removed. Exception: copy-locked blocks keep every bullet unchanged. -2. Include "pr" from the ## PR #NNN header on every bullet. -3. Preserve every ## block so release reconciliation can prove each merged PR is represented. Put no-user-impact / tooling last and collapse it to one concise Changed bullet. -4. Within each category array, order bullets by importance (user-impact-critical first, then high, no-user-impact last). backend-change does not by itself lower priority when paired with a user-impact level. -5. Keep a Changelog category names exactly: Added, Changed, Fixed, Deprecated, Removed, Security. -6. No PR numbers inside "text" (appended separately). No markdown headers in output. -7. Prefer 2–4 bullets per PR cluster total across categories, not one line per commit. - -Tone examples (CHANGELOG — technical): -- "Smart Queue v2: tiered SmartQueueEngine (T0–T4, T3.5) with skip memory, region-aware refill, and unified BoxLorePlaybackService path" -- "EngagementPromptCoordinator: native PostHog NPS surveys, unified NPS/Play review modal, 14-day promoter cooldown" -- "Fixed Tier 0 newest-sort adding archive episodes on latest-item play; discovery landing guard skips deep continuation" - -Do NOT use README listener tone like "shows why each item is there" — use provenance labels, AUTO_FILL, contextSourceId, etc.""" - - user_prompt = f"""Curate these PR/theme clusters into grouped Keep a Changelog bullets: - -{clustered_input}""" - - parsed = _groq_chat_json( - api_key, - system_prompt, - user_prompt, - "Groq CHANGELOG curation error", - ) - - curated: dict[str, list[tuple[str, int | None]]] = defaultdict(list) - for category in CATEGORY_ORDER: - raw = parsed.get(category, []) - if not isinstance(raw, list): - continue - for entry in raw: - text, pr_number = _parse_changelog_bullet(entry) - if text: - curated[category].append((text, pr_number)) - - if curated: - curated_sections = _sections_from_cluster_bullets(curated, clusters) - expected_prs = { - cluster.pr_number - for cluster in clusters - if cluster.pr_number is not None - } - returned_prs = { - pr_number - for bullets in curated_sections.values() - for bullet in bullets - if (pr_number := _extract_pr_number(bullet)) is not None - } - if returned_prs == expected_prs: - return curated_sections - print( - "Groq curation omitted or added PR clusters; " - "using deterministic fallback." - ) - return _fallback_changelog_from_clusters(clusters) - - def _unreleased_bounds(content: str) -> tuple[int, int] | None: match = re.search(r"^## \[Unreleased\]\s*$", content, flags=re.MULTILINE) if not match: @@ -1040,46 +697,6 @@ def _sort_section_bullets(sections: dict[str, list[str]]) -> dict[str, list[str] } -def _merge_locked_and_curated( - locked: dict[str, list[str]], - curated: dict[str, list[str]], -) -> dict[str, list[str]]: - locked_prs = { - _extract_pr_number(bullet) - for bullets in locked.values() - for bullet in bullets - if _extract_pr_number(bullet) is not None - } - merged: dict[str, list[str]] = {} - for category in CATEGORY_ORDER: - locked_bullets = sorted(locked.get(category, []), key=_bullet_sort_key) - curated_bullets = [ - bullet - for bullet in curated.get(category, []) - if _extract_pr_number(bullet) not in locked_prs - ] - combined = locked_bullets + curated_bullets - if combined: - merged[category] = combined - return merged - - -def _sections_excluding_prs( - sections: dict[str, list[str]], - pr_numbers: set[int], -) -> dict[str, list[str]]: - filtered: dict[str, list[str]] = {} - for category, bullets in sections.items(): - kept = [ - bullet - for bullet in bullets - if (pr := _extract_pr_number(bullet)) is None or pr not in pr_numbers - ] - if kept: - filtered[category] = kept - return filtered - - def _locked_readme_groups_from_changelog( content: str, ) -> tuple[list[dict[str, list[str]]], set[int]]: @@ -1098,15 +715,20 @@ def _locked_readme_groups_from_changelog( locked_prs: set[int] = set() for match in README_COPY_BLOCK_RE.finditer(_unreleased_raw(content)): pr_number = int(match.group(1)) + if impact_scores.get(pr_number) == USER_IMPACT_SCORE["no-user-impact"]: + continue + parsed_groups = _parse_readme_copy_inner(match.group(2)) + if not parsed_groups: + continue locked_prs.add(pr_number) - for group in _parse_readme_copy_inner(match.group(2)): + for group in parsed_groups: heading = group["heading"] for bullet in group["bullets"]: grouped[heading].append( ( impact_scores.get(pr_number, 0), pr_number, - _format_readme_bullet(bullet, pr_number), + _format_readme_bullet(bullet, pr_number, preserve_copy=True), ) ) groups = _sort_readme_groups( @@ -1128,80 +750,22 @@ def _locked_readme_groups_from_changelog( return groups, locked_prs -def _drop_prs_from_readme_groups( - groups: list[dict[str, list[str]]], - pr_numbers: set[int], -) -> list[dict[str, list[str]]]: - cleaned: list[dict[str, list[str]]] = [] - for group in groups: - bullets = [ - bullet - for bullet in group.get("bullets") or [] - if (pr := _extract_pr_number(bullet)) is None or pr not in pr_numbers - ] - if bullets: - cleaned.append({"heading": group["heading"], "bullets": bullets}) - return cleaned - - -def _merge_readme_groups( - locked: list[dict[str, list[str]]], - ai_groups: list[dict[str, list[str]]], -) -> list[dict[str, list[str]]]: - by_heading: dict[str, list[str]] = defaultdict(list) - seen: dict[str, set[str]] = defaultdict(set) - for source in (locked, ai_groups): - for group in source: - heading = group["heading"] - for bullet in group.get("bullets") or []: - if bullet in seen[heading]: - continue - seen[heading].add(bullet) - by_heading[heading].append(bullet) - return _sort_readme_groups( - [{"heading": heading, "bullets": bullets} for heading, bullets in by_heading.items() if bullets] - ) - - -def _format_readme_bullet(text: str, pr_number: int | None) -> str: +def _format_readme_bullet(text: str, pr_number: int | None, *, preserve_copy: bool = False) -> str: cleaned = text.strip() if not cleaned: return cleaned - cleaned = re.sub( - r"^(?:\[(?:Fixed|Added|Changed|Improved|Fix|Add|Change|Security)\]|(?:Fixed|Added|Changed|Improved|Fix|Add|Change|Security):)\s*", - "", - cleaned, - flags=re.IGNORECASE, - ).strip() + if not preserve_copy: + cleaned = re.sub( + r"^(?:\[(?:Fixed|Added|Changed|Improved|Fix|Add|Change|Security)\]|(?:Fixed|Added|Changed|Improved|Fix|Add|Change|Security):)\s*", + "", + cleaned, + flags=re.IGNORECASE, + ).strip() if pr_number is not None and not _pr_already_present(cleaned, pr_number): cleaned = f"{cleaned} {_pr_suffix(pr_number)}" return cleaned -def _parse_readme_bullet(entry: object) -> tuple[str, int | None]: - if isinstance(entry, dict): - text = str(entry.get("text", "")).strip() - pr_raw = entry.get("pr") - pr_number = int(pr_raw) if pr_raw is not None else None - return text, pr_number - return str(entry).strip(), None - - -def _dominant_readme_heading(cluster: ChangelogCluster) -> str: - if cluster.impact == "user-impact-critical": - return "Critical" - categories = {cat for cat, _ in cluster.items} - if "Added" in categories: - return "New features" - if "Changed" in categories: - return "Improvements" - if "Fixed" in categories: - return "Fixes" - if "Security" in categories: - return "Security" - return "Other" - - def _readme_eligible(cluster: ChangelogCluster) -> bool: """Whether a cluster should appear in README Upcoming / release highlights.""" if not cluster.items: @@ -1221,113 +785,6 @@ def _readme_eligible(cluster: ChangelogCluster) -> bool: return cluster.importance >= 40 -def _fallback_readme_from_clusters(clusters: list[ChangelogCluster]) -> list[dict[str, list[str]]]: - """Deterministic grouped README when Groq grouping fails.""" - grouped: dict[str, list[str]] = defaultdict(list) - for cluster in clusters: - if not _readme_eligible(cluster): - continue - heading = _dominant_readme_heading(cluster) - preview = cluster.items[0][1] - if len(preview) > 117: - preview = preview[:114].rstrip() + "..." - grouped[heading].append(_format_readme_bullet(preview, cluster.pr_number)) - return _sort_readme_groups( - [{"heading": heading, "bullets": bullets} for heading, bullets in grouped.items() if bullets] - ) - - -def _groq_curate_readme_upcoming( - api_key: str, sections: dict[str, list[str]] -) -> list[dict[str, list[str]]]: - """Curate all [Unreleased] entries into grouped, importance-sorted README bullets.""" - if not any(sections.values()): - return [] - - clusters = _cluster_sections_by_pr(sections) - clustered_input = _render_clustered_changelog(clusters) - if not clustered_input: - return [] - - system_prompt = """You curate clustered changelog entries into a README "Upcoming Changes" section for boxlore listeners. - -Audience: podcast listeners using the app — plain English only. -This section must answer: what is different in the listener's day-to-day use of boxlore? -Do NOT use CHANGELOG-style technical terms (SmartQueueEngine, Tier 0, PlaybackRepository, AUTO_FILL, contextSourceId, composable/class names, FCM payload keys, PackageManager, DataStore). -Never mention: Material 3 / Material You, PostHog, analytics/telemetry/event names, CI, Groq, CodeRabbit, Sonar, refactoring, modules, workflows, or "logs views/actions". -The paired CHANGELOG entry for the same PR uses developer language; this README section must feel different. - -If the PR/cluster text includes a "Listener impact" or "What changes in the user's life" idea, prioritize that outcome over implementation details. - -Input is pre-grouped by PR/theme with an importance score (higher = more user-visible). Each ## block is ONE feature area — merge its bullets into as few README lines as possible. - -Return ONLY valid JSON: -{"groups": [{"heading": "...", "bullets": [{"text": "...", "pr": 853}, ...]}, ...]} - -Each bullet MUST include "pr" copied from the matching ## PR #NNN block in the input. One bullet object per ## block you keep. - -Allowed headings (exactly one per group, omit empty): -- "Critical" — correctness fixes listeners already feel (missing episodes, late alerts). Lead the list. -- "New features" — new capabilities listeners can use (from Added clusters that are truly new surfaces) -- "Improvements" — clearer UI, polish, safer dismissal, better update notes (prefer this for announcement/dialog polish) -- "Fixes" — bugs resolved (from Fixed clusters) -- "Security" — privacy/security only - -MANDATORY clustering rules: -1. ONE README bullet per input ## block in most cases. Never split a single PR/theme across multiple bullets. -2. For importance 90+ themes (queue, playback, discovery): allow at most TWO bullets if they describe clearly distinct user wins; prefer ONE strong sentence. -3. For importance 40–55 themes (NPS, surveys, review prompts): exactly ONE bullet, never lead the list. -4. Drop clusters with importance below 40 / no-user-impact entirely from README. Include user-impact-critical, user-impact-high and user-impact-medium always; user-impact-low when space allows. backend-change may be paired with any user-impact level and does not exclude README by itself. -5. Merge all bullets inside a ## block before writing — e.g. four queue bullets → one: "Smart queue auto-refills, shows why items appear, supports drag reorder, and undo remove." Never rewrite copy-locked blocks. -6. Sort bullets within each group by the cluster importance score (highest first). user-impact-critical MUST appear before high/medium/low. -7. Cap at 8 bullets total across all groups. Copy-locked bullets are exempt from the cap and must be kept. -8. Prefer concrete listener outcomes (easier to read update notes, harder to dismiss by accident, Play installs skip GitHub download prompts) over UI toolkit or logging details. - -Importance guidance (respect the scores in input): -- user-impact-critical / 100: lead README / What's New; author copy if present -- user-impact-high / 90–99: headline features -- user-impact-medium / 70–89: solid README entries -- user-impact-low / 40–55: optional shorter lines -- no-user-impact / below 40: omit from README (CHANGELOG only) - -Rewrite in plain English; no PR numbers inside "text", no Compose/modules jargon. Under 140 chars in "text" (PR link is appended separately).""" - - user_prompt = f"""Curate these PR/theme clusters into grouped README bullets. -Lead with what listeners notice. Ignore analytics and design-system branding. - -{clustered_input}""" - - parsed = _groq_chat_json( - api_key, - system_prompt, - user_prompt, - "Groq README curation error", - ) - raw_groups = parsed.get("groups", []) - if not isinstance(raw_groups, list): - return _fallback_readme_from_clusters(clusters) - - groups: list[dict[str, list[str]]] = [] - for item in raw_groups: - if not isinstance(item, dict): - continue - heading = str(item.get("heading", "")).strip() - bullets_raw = item.get("bullets", []) - if not heading or not isinstance(bullets_raw, list): - continue - bullets: list[str] = [] - for entry in bullets_raw: - text, pr_number = _parse_readme_bullet(entry) - if text: - bullets.append(_format_readme_bullet(text, pr_number)) - if bullets: - groups.append({"heading": heading, "bullets": bullets}) - - if groups: - return _sort_readme_groups(groups) - return _fallback_readme_from_clusters(clusters) - - def _sort_readme_groups(groups: list[dict[str, list[str]]]) -> list[dict[str, list[str]]]: order = {name: index for index, name in enumerate(README_GROUP_ORDER)} @@ -1338,40 +795,6 @@ def rank(group: dict[str, list[str]]) -> tuple[int, str]: return sorted(groups, key=rank) -def _groq_readme_summary(api_key: str, sections: dict[str, list[str]]) -> list[str]: - """Flat bullet fallback when grouped curation returns nothing.""" - if not any(sections.values()): - return [] - - changelog_text = _render_unreleased(sections) - system_prompt = """You rewrite changelog entries as short README bullets for end users of boxlore, a podcast Android app. -Return ONLY valid JSON: {"bullets": ["...", ...]}. - -Rules: -- Write for listeners using the app, not developers. Answer what changed in their day-to-day use. -- One bullet per user-visible change; merge related technical items into one line. -- Never mention: Compose, recomposition, lazy grid, PerfLog, CI, Groq, CodeRabbit, Sonar, refactoring, parameters, modules, workflows, Material 3, PostHog, analytics event names, or internal code names. -- Prefer outcomes like clearer update notes, accidental dismissals avoided, Play installs skipping GitHub download prompts. -- Keep each bullet under 140 characters. -- No leading dashes, PR numbers, or markdown links.""" - - user_prompt = f"""Convert these [Unreleased] changelog entries into user-facing README bullets. -Ignore implementation detail; keep listener-facing outcomes only. - -{changelog_text}""" - - parsed = _groq_chat_json( - api_key, - system_prompt, - user_prompt, - "Groq README summary error", - ) - raw = parsed.get("bullets", []) - if not isinstance(raw, list): - return [] - return [str(item).strip() for item in raw if str(item).strip()] - - def _parse_unreleased_sections(unreleased_block: str) -> dict[str, list[str]]: unreleased_block = README_COPY_BLOCK_RE.sub("", unreleased_block) sections: dict[str, list[str]] = {} @@ -1485,7 +908,7 @@ def _render_whats_new_inner( version_tag: str, release_date: str, body_html: str, - include_ai_notice: bool = True, + include_ai_notice: bool = False, ) -> str: body = body_html.strip() if include_ai_notice: @@ -1521,7 +944,7 @@ def _render_readme_upcoming_body(groups: list[dict[str, list[str]]] | None = Non return EMPTY_UPCOMING_TEXT -def _format_upcoming_inner(body: str, include_ai_notice: bool = True) -> str: +def _format_upcoming_inner(body: str, include_ai_notice: bool = False) -> str: stripped = body.strip() if not stripped: stripped = EMPTY_UPCOMING_TEXT @@ -1538,7 +961,7 @@ def _render_release_notes_shell( *, upcoming_inner: str, whats_new_inner: str | None, - include_ai_notice: bool = True, + include_ai_notice: bool = False, ) -> str: upcoming = _format_upcoming_inner(upcoming_inner, include_ai_notice=include_ai_notice) whats_new_block = "" @@ -1568,7 +991,7 @@ def _render_readme_upcoming_block( content: str, groups: list[dict[str, list[str]]] | None = None, bullets: list[str] | None = None, - include_ai_notice: bool = True, + include_ai_notice: bool = False, ) -> str: """Rewrite the Upcoming body; preserve the latest What's New region if present.""" body = _render_readme_upcoming_body(groups=groups, bullets=bullets) @@ -1603,7 +1026,7 @@ def _update_readme( content: str, groups: list[dict[str, list[str]]] | None = None, bullets: list[str] | None = None, - include_ai_notice: bool = True, + include_ai_notice: bool = False, ) -> str: block = _render_readme_upcoming_block( content, @@ -1616,7 +1039,8 @@ def _update_readme( flags=re.DOTALL, ) if pattern.search(content): - updated = pattern.sub(block, content, count=1) + # Authored text may contain backslashes; never treat it as a regex replacement. + updated = pattern.sub(lambda _: block, content, count=1) else: anchor = re.search( r"^(|## Search\b|

)", @@ -1670,177 +1094,125 @@ def _update_changelog( return _write_unreleased_region(content, merged, copy_blocks), True -def append_changelog( - api_key: str, - pr_number: int, +def validate_release_copy( pr_title: str, pr_body: str, - labels: list[str] | None = None, -) -> bool: - if not CHANGELOG_PATH.exists(): - print("CHANGELOG.md not found", file=sys.stderr) - sys.exit(1) - - impact, backend_change = _resolve_pr_tags( - labels=labels, title=pr_title, body=pr_body - ) - if impact: - backend_note = " + backend-change" if backend_change else "" - print(f"PR #{pr_number} tags: {impact}{backend_note}") - else: - print( - f"::warning::PR #{pr_number} is missing a required user-impact label " - f"({', '.join(USER_IMPACT_LABELS)}). " - f"Optional pairable label: {BACKEND_CHANGE_LABEL}. " - "Prioritization will use theme heuristics only." + labels: list[str] | None, +) -> tuple[str, bool, dict[str, list[str]], list[dict[str, list[str]]]]: + """Validate author copy before writing files; never synthesize missing wording.""" + impacts = {label for label in labels or [] if label in USER_IMPACT_LABELS} + if len(impacts) != 1: + raise ValueError( + "Add exactly one PR impact label: " + ", ".join(USER_IMPACT_LABELS) + + ". Then rerun release-copy validation." ) - if backend_change: - print(f"PR #{pr_number} also has backend-change") - - changelog_original = CHANGELOG_PATH.read_text(encoding="utf-8") - locked_entries = parse_pr_changelog_copy(pr_body, pr_title, impact) - copy_locked = bool(locked_entries) - if copy_locked: - print(f"PR #{pr_number}: using verbatim CHANGELOG copy from the PR body (not Groq).") - entries = locked_entries - else: - if impact == "user-impact-critical": - print( - f"::warning::PR #{pr_number} is user-impact-critical but " - "release-copy:changelog is empty; falling back to Groq." - ) - entries = _groq_entries(api_key, pr_number, pr_title, pr_body) - if not entries: - print( - f"Groq returned no changelog entries for PR #{pr_number}; " - "synthesizing a Changed/Added/Fixed bullet from the PR title." + impact = next(iter(impacts)) + backend = any(_is_backend_label(label) for label in labels or []) + for name, start, end in ( + ("changelog", PR_CHANGELOG_COPY_START, PR_CHANGELOG_COPY_END), + ("readme", PR_README_COPY_START, PR_README_COPY_END), + ): + if name == "readme" and impact == "no-user-impact": + continue + inner = _extract_marked_region(pr_body, start, end) + if pr_body.count(start) != 1 or pr_body.count(end) != 1 or inner is None: + raise ValueError( + f"Keep exactly one complete release-copy:{name} region in the PR body. " + "Use the PR template markers, then rerun validation." ) - entries = _fallback_entries_from_title(pr_title) - if not entries: - print("Could not synthesize a changelog entry from the PR title.") - return False - - changelog_updated, changelog_changed = _update_changelog( - changelog_original, - entries, - pr_number, - impact=impact, - backend_change=backend_change, - copy_locked=copy_locked, - ) - if changelog_changed: - changelog_original = changelog_updated - - readme_groups = parse_pr_readme_copy(pr_body, impact) - if readme_groups: - print(f"PR #{pr_number}: storing verbatim README copy for Upcoming / What's New.") - with_copy = _upsert_readme_copy_block(changelog_original, pr_number, readme_groups) - if with_copy != changelog_original: - changelog_original = with_copy - changelog_changed = True - elif impact == "user-impact-critical": - print( - f"::warning::PR #{pr_number} is user-impact-critical but " - "release-copy:readme is empty; README will be Groq-curated." + for line in inner.splitlines(): + bullet = re.match(r"^[-*]\s*(.*)$", line.strip()) + if bullet and _is_placeholder_bullet(bullet.group(1)): + raise ValueError( + f"Replace or remove the empty/TBD bullet in release-copy:{name}. " + "Every release note must have authored wording." + ) + entries = parse_pr_changelog_copy(pr_body, pr_title, impact) + if not entries: + raise ValueError( + "Fill the PR's release-copy:changelog region with categorized developer bullets. " + "Empty bullets and TBD are placeholders. Save the PR body, then rerun the failed workflow." ) + groups = parse_pr_readme_copy(pr_body, impact) + if impact != "no-user-impact" and not groups: + raise ValueError( + "Fill the PR's release-copy:readme region with listener-facing changes. " + "Save the PR body, then rerun the failed workflow. No automatic copy is generated." + ) + if impact == "no-user-impact": + groups = [] + return impact, backend, entries, groups - if changelog_changed: - CHANGELOG_PATH.write_text(changelog_original, encoding="utf-8") - print(f"Updated CHANGELOG.md for PR #{pr_number}.") - else: - print(f"No CHANGELOG changes for PR #{pr_number}.") - return changelog_changed - - -def sync_changelog_unreleased(api_key: str) -> bool: - if not CHANGELOG_PATH.exists(): - print("CHANGELOG.md not found", file=sys.stderr) - sys.exit(1) +def append_changelog( + pr_number: int, + pr_title: str, + pr_body: str, + labels: list[str] | None = None, +) -> bool: original = CHANGELOG_PATH.read_text(encoding="utf-8") - unreleased = _extract_unreleased_sections(original) - copy_blocks = _collect_readme_copy_blocks(_unreleased_raw(original)) - if not unreleased and not copy_blocks: - print("No [Unreleased] entries to curate.") + # Historical entries without authored copy remain readable and idempotent. + if _pr_already_present(original, pr_number): + if not _pr_already_present(_unreleased_raw(original), pr_number): + return False + groups = parse_pr_readme_copy(pr_body) + impact, _ = _resolve_pr_tags(labels) + if groups and impact != "no-user-impact" and pr_number not in _locked_readme_groups_from_changelog(original)[1]: + updated = _upsert_readme_copy_block(original, pr_number, groups) + if updated != original: + CHANGELOG_PATH.write_text(updated, encoding="utf-8") + return True + print(f"CHANGELOG already contains PR #{pr_number}; no entry rewritten.") return False + impact, backend, entries, groups = validate_release_copy(pr_title, pr_body, labels) + updated, changed = _update_changelog( + original, entries, pr_number, + impact=impact, backend_change=backend, copy_locked=True, + ) + if groups: + updated = _upsert_readme_copy_block(updated, pr_number, groups) + if changed: + CHANGELOG_PATH.write_text(updated, encoding="utf-8") + print(f"Stored authored release copy for PR #{pr_number}.") + return changed - locked, unlocked = _partition_locked_sections(unreleased) - if not any(unlocked.values()): - print("All [Unreleased] bullets are copy-locked; skipping Groq CHANGELOG curation.") - curated = _sort_section_bullets(locked) - else: - print("Curating unlocked CHANGELOG bullets with Groq; preserving copy-locked bullets.") - groq_curated = _groq_curate_changelog_unreleased(api_key, unlocked) - if not groq_curated and not any(locked.values()): - print("CHANGELOG curation produced no entries.") - return False - curated = _merge_locked_and_curated(locked, groq_curated or {}) - updated = _replace_unreleased_sections(original, curated, copy_blocks=copy_blocks) +def sync_changelog_unreleased() -> bool: + original = CHANGELOG_PATH.read_text(encoding="utf-8") + sections = _extract_unreleased_sections(original) + copy_blocks = _collect_readme_copy_blocks(_unreleased_raw(original)) + updated = _replace_unreleased_sections( + original, _sort_section_bullets(sections), copy_blocks=copy_blocks, + ) if updated != original: CHANGELOG_PATH.write_text(updated, encoding="utf-8") - print("Re-grouped CHANGELOG.md [Unreleased] section.") + print("Ordered CHANGELOG bullets by category, impact and PR; wording unchanged.") return True - - print("No CHANGELOG changes written.") return False -def sync_readme_upcoming(api_key: str) -> bool: - if not CHANGELOG_PATH.exists(): - print("CHANGELOG.md not found", file=sys.stderr) - sys.exit(1) - if not README_PATH.exists(): - print("README.md not found", file=sys.stderr) - sys.exit(1) +def reviewed_readme_groups(content: str) -> list[dict[str, list[str]]]: + groups, copied_prs = _locked_readme_groups_from_changelog(content) + clusters = _cluster_sections_by_pr(_extract_unreleased_sections(content)) + missing = [c.pr_number for c in clusters if _readme_eligible(c) and c.pr_number not in copied_prs] + if missing: + names = ", ".join(f"#{number}" if number is not None else "an unlinked entry" for number in missing) + raise ValueError( + "Missing listener release copy for " + names + ". Fill each PR's " + "release-copy:readme region, run backfill-changelog for that PR, then retry. " + "No README entries were rewritten or omitted." + ) + return groups - readme_original = README_PATH.read_text(encoding="utf-8") - changelog_content = CHANGELOG_PATH.read_text(encoding="utf-8") - unreleased = _extract_unreleased_sections(changelog_content) - if not unreleased: - print("No [Unreleased] entries; clearing README upcoming section.") - readme_updated = _update_readme(readme_original) - else: - locked_groups, locked_prs = _locked_readme_groups_from_changelog(changelog_content) - unlocked = _sections_excluding_prs(unreleased, locked_prs) - unlocked_clusters = [c for c in _cluster_sections_by_pr(unlocked) if _readme_eligible(c)] - ai_groups: list[dict[str, list[str]]] = [] - if unlocked_clusters: - print("Curating README from unlocked [Unreleased] changelog entries...") - ai_groups = _groq_curate_readme_upcoming(api_key, unlocked) - if not ai_groups: - print("Grouped curation empty; falling back to cluster bullets.") - ai_groups = _fallback_readme_from_clusters(unlocked_clusters) - ai_groups = _drop_prs_from_readme_groups(ai_groups, locked_prs) - elif locked_groups: - print("Using verbatim README copy; skipping Groq README curation.") - else: - all_clusters = [c for c in _cluster_sections_by_pr(unreleased) if _readme_eligible(c)] - if all_clusters: - print("Curating README from all [Unreleased] changelog entries...") - ai_groups = _groq_curate_readme_upcoming(api_key, unreleased) - if not ai_groups: - print("Grouped curation empty; falling back to cluster bullets.") - ai_groups = _fallback_readme_from_clusters(all_clusters) - - groups = _merge_readme_groups(locked_groups, ai_groups) - include_ai_notice = bool(ai_groups) - if groups: - readme_updated = _update_readme( - readme_original, - groups=groups, - include_ai_notice=include_ai_notice, - ) - else: - readme_updated = _update_readme(readme_original) - - if readme_updated != readme_original: - README_PATH.write_text(readme_updated, encoding="utf-8") - print("Synced README.md Upcoming Changes section.") +def sync_readme_upcoming() -> bool: + original = README_PATH.read_text(encoding="utf-8") + groups = reviewed_readme_groups(CHANGELOG_PATH.read_text(encoding="utf-8")) + updated = _update_readme(original, groups=groups, include_ai_notice=False) + if updated != original: + README_PATH.write_text(updated, encoding="utf-8") + print("Synced authored README copy; category order and impact determine placement.") return True - - print("No README changes written.") return False @@ -1869,8 +1241,8 @@ def main() -> None: "command", nargs="?", default="all", - choices=("all", "append", "sync-changelog", "sync-readme"), - help="append: write PR to CHANGELOG; sync-changelog: re-group [Unreleased]; sync-readme: curate README; all: append + sync-changelog + sync-readme", + choices=("all", "append", "sync-changelog", "sync-readme", "validate-copy"), + help="append: write PR to CHANGELOG; sync-changelog: re-group [Unreleased]; sync-readme: order authored README copy; validate-copy: check a PR event; all: append + sync-changelog + sync-readme", ) parser.add_argument( "--pr-json", @@ -1878,9 +1250,18 @@ def main() -> None: ) args = parser.parse_args() - api_key = _require_env("GROQ_API_KEY") + if args.command == "validate-copy": + event = json.loads(Path(_require_env("GITHUB_EVENT_PATH")).read_text(encoding="utf-8")) + pull = event["pull_request"] + title = str(pull.get("title") or "") + if "[skip changelog]" in title or str(pull.get("head", {}).get("ref", "")).startswith("release/v"): + print("Release/tooling PR opts out of changelog copy.") + return + validate_release_copy(title, str(pull.get("body") or ""), _labels_from_env_or_payload(pull)) + print("Release copy is ready: authored wording will be preserved.") + return changelog_changed = False - changelog_curated = False + changelog_ordered = False readme_changed = False if args.command in ("all", "append"): @@ -1892,18 +1273,22 @@ def main() -> None: pr_body = os.environ.get("PR_BODY", "") labels = _labels_from_env_or_payload() changelog_changed = append_changelog( - api_key, pr_number, pr_title, pr_body, labels=labels + pr_number, pr_title, pr_body, labels=labels ) if args.command in ("all", "sync-changelog"): - changelog_curated = sync_changelog_unreleased(api_key) + changelog_ordered = sync_changelog_unreleased() if args.command in ("all", "sync-readme"): - readme_changed = sync_readme_upcoming(api_key) + readme_changed = sync_readme_upcoming() - if not changelog_changed and not changelog_curated and not readme_changed: + if not changelog_changed and not changelog_ordered and not readme_changed: print("No CHANGELOG or README changes written.") if __name__ == "__main__": - main() + try: + main() + except (ValueError, OSError, KeyError) as error: + print(f"Release copy check failed: {error}", file=sys.stderr) + sys.exit(1) diff --git a/.github/scripts/verify_release_contracts.py b/.github/scripts/verify_release_contracts.py index 21c7b8cd4..c1d472741 100644 --- a/.github/scripts/verify_release_contracts.py +++ b/.github/scripts/verify_release_contracts.py @@ -33,6 +33,7 @@ DEBUG_ONLY = {"com/google/firebase/appcheck/debug/DebugAppCheckProviderFactory", "com/google/firebase/appcheck/debug/FirebaseAppCheckDebugRegistrar", APP_PREFIX + "debug/TestNotificationReceiver"} +DEBUG_ONLY.add(APP_PREFIX + "testing/AnnouncementTestReceiver") WIDGET_MODELS = {APP_PREFIX + "feature/widgets/" + name for name in ("NowPlayingWidgetSnapshot", "LibraryWidgetSnapshot", "WidgetShowRow", "WidgetEpisodeRow")} @@ -258,17 +259,33 @@ def main() -> int: parser.add_argument("--root", type=Path, default=Path(__file__).resolve().parents[2]) parser.add_argument("--sdk", type=Path) parser.add_argument("--report", type=Path) + parser.add_argument("--app-variant", choices=("release", "playRelease"), default="release") args = parser.parse_args() root = args.root.resolve() sdk = args.sdk or Path(os.environ.get("ANDROID_SDK_ROOT") or os.environ.get("ANDROID_HOME") or "") try: - classes = load_project_classes(root) - mapping = read_mapping(root / "app/build/outputs/mapping/release/mapping.txt") - manifest = manifest_constructors(root / "app/build/intermediates/merged_manifests/release/processReleaseManifest/AndroidManifest.xml") + variant = args.app_variant + task_variant = variant[0].upper() + variant[1:] + classes = load_project_classes(root, variant) + mapping = read_mapping(root / f"app/build/outputs/mapping/{variant}/mapping.txt") + manifest_path = root / f"app/build/intermediates/merged_manifests/{variant}/process{task_variant}Manifest/AndroidManifest.xml" + manifest = manifest_constructors(manifest_path) + permission = "android.permission.REQUEST_INSTALL_PACKAGES" + permissions = {item.get("{http://schemas.android.com/apk/res/android}name") for item in ET.parse(manifest_path).getroot().iter("uses-permission")} + if any(item.get("{http://schemas.android.com/apk/res/android}name") == "cx.aswin.boxlore.ISOLATED_UPDATE_TEST" for item in ET.parse(manifest_path).getroot().iter("meta-data")): + raise ValueError("Isolated test build cannot be published") + if variant == "playRelease" and permission in permissions: + raise ValueError("Play bundle must not request APK installation permission") + if variant == "release" and permission not in permissions: + raise ValueError("Direct APK installer permission is missing") report = {} for artifact in args.artifacts: dex, size = inspect_archive(artifact, sdk_tool("dexdump", sdk)) counts = validate_contracts(classes, dex, mapping, manifest) + if variant == "playRelease": + for name in (APP_PREFIX + "updates/DirectApkInstaller", APP_PREFIX + "updates/ApkFileDownloader"): + if mapping.get(name, name) in dex: + raise ValueError("APK installer code packaged in Play bundle: " + name) counts["raw_uri_payloads"] = validate_raw_uri_payloads(artifact, root, literal_uri_resources(root)) if artifact.suffix == ".apk": counts["dynamic_resources"] = validate_resources(artifact, root, sdk_tool("aapt2", sdk)) diff --git a/.github/workflows/changelog-on-merge.yml b/.github/workflows/changelog-on-merge.yml index bedb3a10f..dfddca849 100644 --- a/.github/workflows/changelog-on-merge.yml +++ b/.github/workflows/changelog-on-merge.yml @@ -30,10 +30,10 @@ on: release_notes_source: description: Release-note source for prepare-release required: true - default: reconcile-with-ai + default: reconcile-reviewed-copy type: choice options: - - reconcile-with-ai + - reconcile-reviewed-copy - readme-upcoming skip_notify: description: >- @@ -65,7 +65,7 @@ jobs: contents: write pull-requests: read concurrency: - group: changelog-on-merge + group: boxlore-release-metadata cancel-in-progress: false steps: - name: Generate boxlore pusher token @@ -84,7 +84,6 @@ jobs: - name: Append merged PR event to CHANGELOG if: github.event_name == 'pull_request' env: - GROQ_API_KEY: ${{ secrets.REVIEW_KEY }} PR_NUMBER: ${{ github.event.pull_request.number }} PR_TITLE: ${{ github.event.pull_request.title }} PR_BODY: ${{ github.event.pull_request.body }} @@ -95,7 +94,6 @@ jobs: if: github.event_name == 'workflow_dispatch' env: GH_TOKEN: ${{ steps.app-token.outputs.token }} - GROQ_API_KEY: ${{ secrets.REVIEW_KEY }} PR_NUMBER: ${{ github.event.inputs.pr_number }} run: | if ! [[ "$PR_NUMBER" =~ ^[0-9]+$ ]]; then @@ -109,14 +107,10 @@ jobs: python3 .github/scripts/update_changelog.py append \ --pr-json "$RUNNER_TEMP/pr.json" - - name: Curate CHANGELOG grouping and priority - env: - GROQ_API_KEY: ${{ secrets.REVIEW_KEY }} + - name: Order authored CHANGELOG bullets run: python3 .github/scripts/update_changelog.py sync-changelog - - name: Curate README from all unreleased changes - env: - GROQ_API_KEY: ${{ secrets.REVIEW_KEY }} + - name: Place authored README copy by category and impact run: python3 .github/scripts/update_changelog.py sync-readme - name: Commit and push changelog update @@ -145,6 +139,21 @@ jobs: echo "::error::Failed to push CHANGELOG update after 5 attempts" exit 1 + - name: Refresh Coming next in the verified update feed + env: + GH_TOKEN: ${{ steps.app-token.outputs.token }} + run: | + TAG=$(gh release view --repo "$GITHUB_REPOSITORY" --json tagName --jq '.tagName') + mkdir -p "$RUNNER_TEMP/upcoming-feed" + if ! gh release download "$TAG" --repo "$GITHUB_REPOSITORY" --pattern update.json --dir "$RUNNER_TEMP/upcoming-feed"; then + echo "::warning::Upcoming preview not published: the latest release has no accessible update feed. Run publication after fixing access or creating the verified feed." + exit 0 + fi + git pull --ff-only origin master + python3 .github/scripts/refresh_upcoming.py --manifest "$RUNNER_TEMP/upcoming-feed/update.json" + gh release upload "$TAG" "$RUNNER_TEMP/upcoming-feed/update.json" --repo "$GITHUB_REPOSITORY" --clobber + echo "Updated Coming next only. No APK changed and no notification was sent." >> "$GITHUB_STEP_SUMMARY" + prepare-release: if: >- github.event_name == 'workflow_dispatch' && @@ -154,7 +163,7 @@ jobs: contents: write pull-requests: write concurrency: - group: prepare-release + group: boxlore-release-metadata cancel-in-progress: false steps: - name: Generate boxlore pusher token @@ -190,7 +199,6 @@ jobs: id: release env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - GROQ_API_KEY: ${{ secrets.REVIEW_KEY }} BUMP: ${{ github.event.inputs.bump }} LATEST_TAG: ${{ steps.latest.outputs.tag }} RELEASE_NOTES_SOURCE: ${{ github.event.inputs.release_notes_source }} @@ -310,9 +318,9 @@ jobs: )" else if [[ "$NOTES_SOURCE" == "readme-upcoming" ]]; then - notes_message="README Upcoming is promoted verbatim into What's New and the in-app notification; AI reconciliation is skipped." + notes_message="Reviewed README Upcoming is promoted verbatim into What's New and the in-app notification." else - notes_message="Changelog and README use author-written release-copy when present; remaining notes are reconciled before promotion." + notes_message="Changelog and README use the required author-written release copy, ordered without rewriting before promotion." fi title="release: ${TAG} [skip changelog]" body="$(cat <- needs.publish-release.result == 'success' && @@ -1250,8 +1296,13 @@ jobs: type: in-app route: ${{ needs.publish-release.outputs.notification_route }} image: '' - target: all_users + target: direct_users dry_run: 'false' + preview_only: 'true' + release_url: ${{ format('https://github.com/boxcreate/boxlore/releases/tag/{0}', needs.publish-release.outputs.tag) }} + release_alert: 'true' + release_version_code: ${{ needs.publish-release.outputs.version_code }} + presentation: fullscreen collapse_key: ${{ format('boxlore-release-{0}', needs.publish-release.outputs.tag) }} sound: silent action_label: Download diff --git a/.github/workflows/release-contracts.yml b/.github/workflows/release-contracts.yml index 7e16d6f3b..6c2b8c221 100644 --- a/.github/workflows/release-contracts.yml +++ b/.github/workflows/release-contracts.yml @@ -34,14 +34,19 @@ jobs: - name: Build optimized unsigned APK and bundle run: >- ./gradlew --no-daemon -I .github/scripts/release_verification.init.gradle - :app:assembleRelease :app:bundleRelease :app:lintRelease - -x :app:uploadCrashlyticsMappingFileRelease + :app:assembleRelease :app:bundlePlayRelease :app:lintPlayRelease :app:lintRelease + -x :app:uploadCrashlyticsMappingFileRelease -x :app:uploadCrashlyticsMappingFilePlayRelease - name: Verify optimized release contracts run: >- python3 .github/scripts/verify_release_contracts.py --report app/build/reports/release-contracts.json app/build/outputs/apk/release/app-release-unsigned.apk - app/build/outputs/bundle/release/app-release.aab + - name: Verify optimized Play contracts and installer exclusion + run: >- + python3 .github/scripts/verify_release_contracts.py + --app-variant playRelease + --report app/build/reports/play-release-contracts.json + app/build/outputs/bundle/playRelease/app-playRelease.aab - name: Preserve release diagnostics if: always() uses: actions/upload-artifact@v4 @@ -50,12 +55,14 @@ jobs: retention-days: 14 path: | app/build/reports/release-contracts.json + app/build/reports/play-release-contracts.json app/build/reports/lint-results-release.* app/build/outputs/mapping/release/configuration.txt app/build/outputs/mapping/release/mapping.txt app/build/outputs/mapping/release/seeds.txt app/build/outputs/mapping/release/usage.txt app/build/outputs/mapping/release/missing_rules.txt + app/build/outputs/mapping/playRelease/ - name: Remove build-only Firebase stub if: always() run: rm -f app/google-services.json diff --git a/.github/workflows/release-copy.yml b/.github/workflows/release-copy.yml new file mode 100644 index 000000000..4c4d49e71 --- /dev/null +++ b/.github/workflows/release-copy.yml @@ -0,0 +1,23 @@ +name: Release copy + +on: + pull_request: + types: [opened, synchronize, reopened, edited, labeled, unlabeled, ready_for_review] + +permissions: + contents: read + +concurrency: + group: release-copy-${{ github.event.pull_request.number }} + cancel-in-progress: true + +jobs: + validate: + name: Authored release copy + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v5 + with: + persist-credentials: false + - name: Check impact label and exact release wording + run: python3 .github/scripts/update_changelog.py validate-copy diff --git a/.github/workflows/release-rehearsal.yml b/.github/workflows/release-rehearsal.yml new file mode 100644 index 000000000..a9c1c0e42 --- /dev/null +++ b/.github/workflows/release-rehearsal.yml @@ -0,0 +1,61 @@ +name: Offline release rehearsal + +on: + workflow_dispatch: + inputs: + operation: + description: Preview a new release or quiet same-version refresh + type: choice + default: prepare-release + options: [prepare-release, refresh-latest-artifacts] + bump: + description: Preview version bump for a new release + type: choice + default: patch + options: [patch, minor, major] + +permissions: + contents: read + +jobs: + rehearse: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v5 + with: + persist-credentials: false + - name: Set up Node for announcement preview checks + uses: actions/setup-node@v5 + with: + node-version: '22' + - name: Announcement composer contract checks + run: node --test .github/scripts/test_announcement_admin.cjs + - name: Test release tooling without credentials + run: python3 -m unittest discover -s .github/scripts -p 'test_*.py' -v + - name: Prepare preview files + env: + OPERATION: ${{ inputs.operation }} + BUMP: ${{ inputs.bump }} + run: | + python3 .github/scripts/rehearse_release.py \ + --output "$RUNNER_TEMP/release-preview" \ + --operation "$OPERATION" --bump "$BUMP" + - name: Save preview files + uses: actions/upload-artifact@v4 + with: + name: release-preview + path: ${{ runner.temp }}/release-preview/ + if-no-files-found: error + - name: Explain result and recovery + if: always() + run: | + cat >> "$GITHUB_STEP_SUMMARY" <<'SUMMARY' + ## Release rehearsal + Nothing was published or sent. Source version files and download links were unchanged. + + **Passed:** Download the `release-preview` artifact. Review candidate notes and `report.json`. + + **Failed:** Open the first failed step. If listener copy is missing, fill that PR's release-copy region, run Backfill changelog, and retry. If a test failed, fix the reported regression and rerun this rehearsal. Do not run Publish release to repair a rehearsal failure. + + This rehearsal covers local copy, version calculation and announcement payloads. APK builds, update installation, provider delivery and remote merged-PR reconciliation need separate checks. + SUMMARY diff --git a/.github/workflows/send-notification.yml b/.github/workflows/send-notification.yml index b2c71bf3f..c30f65539 100644 --- a/.github/workflows/send-notification.yml +++ b/.github/workflows/send-notification.yml @@ -35,6 +35,8 @@ on: - all_users - debug_users - prod_users + - direct_users + - test_users dry_run: description: 'Dry Run Mode (Validate only)' required: true @@ -43,6 +45,18 @@ on: options: - 'false' - 'true' + test_mode: + description: 'Test mode — only isolated test or debug builds may receive this message' + required: false + default: 'false' + type: choice + options: ['false', 'true'] + preview_only: + description: 'Offline preview — no message sent and no Firebase access' + required: false + default: 'false' + type: choice + options: ['false', 'true'] collapse_key: description: 'Collapse Key (Optional)' required: false @@ -80,7 +94,42 @@ on: category: description: 'In-App category badge label' required: true - default: "WHAT'S NEW" + default: "ANNOUNCEMENT" + presentation: + description: In-app appearance + type: choice + default: compact + options: [compact, fullscreen] + tone: + description: Accent role + type: choice + default: primary + options: [primary, secondary, tertiary, error] + image_style: + description: Image layout + type: choice + default: banner + options: [banner, cover] + release_url: + description: GitHub release page for the View on GitHub action + type: string + default: '' + release_alert: + description: GitHub release alert (normally excludes Google Play) + type: boolean + default: false + include_play: + description: Explicitly include Google Play for this release alert + type: boolean + default: false + release_version_code: + description: Release code (0 for ordinary alerts) + type: string + default: '0' + approval_digest: + description: Digest from the reviewed offline preview (required for delivery) + type: string + default: '' workflow_call: inputs: title: @@ -114,6 +163,16 @@ on: required: false default: 'false' type: string + test_mode: + description: 'Test mode — only isolated test or debug builds may receive this message' + required: false + default: 'false' + type: string + preview_only: + description: 'Offline preview — no message sent and no Firebase access' + required: false + default: 'false' + type: string collapse_key: description: 'Optional Android collapse key' required: false @@ -142,8 +201,32 @@ on: category: description: 'In-app category badge' required: false - default: "WHAT'S NEW" + default: "ANNOUNCEMENT" + type: string + presentation: + type: string + default: compact + tone: + type: string + default: primary + image_style: + type: string + default: banner + release_url: + type: string + default: '' + release_alert: type: string + default: 'false' + include_play: + type: string + default: 'false' + release_version_code: + type: string + default: '0' + approval_digest: + type: string + default: '' secrets: FCM_SERVICE_ACCOUNT_KEY: description: 'Firebase service-account JSON' @@ -164,16 +247,25 @@ jobs: - name: Set up Python uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5 with: - python-version: '3.9' + python-version: '3.12' - name: Install dependencies + if: inputs.preview_only != 'true' run: | python -m pip install --upgrade pip pip install firebase-admin - - name: Send Notification + - name: Preview, validate or send announcement env: - FIREBASE_CREDENTIALS: ${{ secrets.FCM_SERVICE_ACCOUNT_KEY }} + FIREBASE_CREDENTIALS: ${{ inputs.preview_only != 'true' && secrets.FCM_SERVICE_ACCOUNT_KEY || '' }} + PRESENTATION: ${{ inputs.presentation || 'compact' }} + TONE: ${{ inputs.tone || 'primary' }} + IMAGE_STYLE: ${{ inputs.image_style || 'banner' }} + RELEASE_URL: ${{ inputs.release_url || '' }} + RELEASE_ALERT: ${{ inputs.release_alert || 'false' }} + INCLUDE_PLAY: ${{ inputs.include_play || 'false' }} + RELEASE_VERSION_CODE: ${{ inputs.release_version_code || '0' }} + APPROVAL_DIGEST: ${{ inputs.approval_digest }} NOTIFICATION_TITLE: ${{ inputs.title }} NOTIFICATION_BODY: ${{ inputs.body }} NOTIFICATION_TYPE: ${{ inputs.type }} @@ -181,6 +273,8 @@ jobs: NOTIFICATION_IMAGE: ${{ inputs.image }} NOTIFICATION_TARGET: ${{ inputs.target }} NOTIFICATION_DRY_RUN: ${{ inputs.dry_run }} + NOTIFICATION_TEST_MODE: ${{ inputs.test_mode || 'false' }} + NOTIFICATION_PREVIEW_ONLY: ${{ inputs.preview_only || 'false' }} NOTIFICATION_COLLAPSE_KEY: ${{ inputs.collapse_key }} NOTIFICATION_SOUND: ${{ inputs.sound }} NOTIFICATION_ACTION_LABEL: ${{ inputs.action_label }} @@ -189,6 +283,9 @@ jobs: NOTIFICATION_CATEGORY: ${{ inputs.category }} run: | python .github/scripts/manual_dispatch.py \ + --release-url "$RELEASE_URL" --presentation "$PRESENTATION" --tone "$TONE" --image-style "$IMAGE_STYLE" \ + --release-alert "$RELEASE_ALERT" --include-play "$INCLUDE_PLAY" \ + --release-version-code "$RELEASE_VERSION_CODE" --approval-digest "$APPROVAL_DIGEST" \ --title "$NOTIFICATION_TITLE" \ --body "$NOTIFICATION_BODY" \ --type "$NOTIFICATION_TYPE" \ @@ -196,9 +293,20 @@ jobs: --image "$NOTIFICATION_IMAGE" \ --target "$NOTIFICATION_TARGET" \ --dry-run "$NOTIFICATION_DRY_RUN" \ + --test-mode "$NOTIFICATION_TEST_MODE" \ + --preview-only "$NOTIFICATION_PREVIEW_ONLY" \ + --preview-output "$RUNNER_TEMP/announcement-preview.json" \ --collapse-key "$NOTIFICATION_COLLAPSE_KEY" \ --sound "$NOTIFICATION_SOUND" \ --action-label "$NOTIFICATION_ACTION_LABEL" \ --show-action-in-push "$NOTIFICATION_SHOW_ACTION_IN_PUSH" \ --show-action-in-app "$NOTIFICATION_SHOW_ACTION_IN_APP" \ --category "$NOTIFICATION_CATEGORY" + + - name: Save offline preview + if: inputs.preview_only == 'true' + uses: actions/upload-artifact@v4 + with: + name: announcement-preview + path: ${{ runner.temp }}/announcement-preview.json + if-no-files-found: error diff --git a/.github/workflows/unit-tests.yml b/.github/workflows/unit-tests.yml index c600d6080..4d8a2675b 100644 --- a/.github/workflows/unit-tests.yml +++ b/.github/workflows/unit-tests.yml @@ -56,6 +56,12 @@ jobs: with: persist-credentials: false + - name: Set up Node for announcement preview checks + uses: actions/setup-node@v5 + with: + node-version: '22' + - name: Announcement composer contract checks + run: node --test .github/scripts/test_announcement_admin.cjs - name: Changelog and release-note script tests run: python3 -m unittest discover -s .github/scripts -p 'test_*.py' -v diff --git a/.gitignore b/.gitignore index 38ea77be6..3cd9f1b8e 100644 --- a/.gitignore +++ b/.gitignore @@ -42,7 +42,18 @@ bench_node.js proxy/ # Sub-projects (local only) -admin-panel/ +# Composer sources are public; the existing gateway and configuration stay local. +admin-panel/* +!admin-panel/README.md +!admin-panel/public/ +admin-panel/public/* +!admin-panel/public/notifyappusers/ +admin-panel/public/notifyappusers/* +!admin-panel/public/notifyappusers/dashboard.html +!admin-panel/public/notifyappusers/dashboard.js +!admin-panel/public/notifyappusers/composer.css +!admin-panel/public/notifyappusers/preview.html +!admin-panel/public/notifyappusers/announcements/ telemetry-worker/ # Local website checkout; keep the rest of docs/ tracked. diff --git a/admin-panel/README.md b/admin-panel/README.md new file mode 100644 index 000000000..702c7cf06 --- /dev/null +++ b/admin-panel/README.md @@ -0,0 +1,19 @@ +# Announcement composer + +## Purpose + +The static composer creates legacy-compatible FCM announcement drafts, previews the native app's visual design, and requires explicit review before a workflow request. The existing Google/password/TOTP gateway and its encrypted configuration remain local and unchanged. The composer never stores or reads sender credentials beyond the gateway's existing in-memory session. + +## Behavior + +- Android renders compact and full-screen announcements in native Compose. The browser preview mirrors their Material 3 surfaces, typography, spacing and actions, using the same Google Sans font. Preview dimensions are content dp excluding system bars; selected Classic colors and text scale match that profile. The surrounding app page is omitted. Actual custom themes, system bars and system notifications are device-owned and cannot be represented as a universal screenshot. +- Accent roles, banner/uncropped cover, Markdown headings, callouts, bullets and emphasis are controlled presentation options. Raw HTML is text. The native app formats text and invokes its already-parsed actions; it has no embedded browser or JavaScript bridge. +- Drafts default to isolated test builds and provider validation. Test mode rejects production audiences. A reviewed draft is snapshotted, and edits invalidate approval. Its SHA-256 digest is checked again by the workflow before real delivery. Workflow acceptance is not device delivery confirmation. +- In-app release alerts always have Download update, View on GitHub and Dismiss. Download enters the native updater; View on GitHub preserves the alert on return. Content, Appearance and Delivery are separate editor tabs, and the final review shows the complete selected viewport and delivery details. Narrow screens open at the preview and scroll to approval; notification-only delivery shows its exact text instead of an in-app alert. Android owns the system notification layout. Ordinary announcements can reach both install channels. GitHub release announcements normally target direct installs, with an explicit manual include-Play override. Release delivery uses the new direct_users audience, excluding Play/test topics; older clients on prod_users are not classifiable and are not included until updated. Automatic publication produces a review artifact, never sends automatically; import that artifact into the composer to edit, review and approve. +- Saved drafts are local browser data. Existing gateway configuration, tokens, backups and hosting configuration remain ignored; deploying this panel is separate from pushing Android changes. + +## Development and verification + +Edit browser presentation assets here when the native visual specification changes. Run `python3 .github/scripts/sync_announcement_preview.py` after app-font changes; `--check` checks the font only. Verify style and layout against phone screenshots for compact/full-screen, light/dark and enlarged text. Browser previews exclude Android system bars and cannot promise identical rendering across devices. Serve this directory locally and open `preview.html` for an unauthenticated offline composer; without a gateway session it cannot dispatch. A `preview.html` screen never grants sender access. + +Run `node --test .github/scripts/test_announcement_admin.cjs` and the Python release-tooling suite. Test preview/digest parity, edited approval, UTF-8 bounds, test audience rejection and both large-text layouts. Native JVM tests cover persistence, legacy fallback, audience/version filtering and process/permission return. Device acceptance is still required for Android permission and installation prompts. diff --git a/admin-panel/public/notifyappusers/announcements/font.ttf b/admin-panel/public/notifyappusers/announcements/font.ttf new file mode 100644 index 0000000000000000000000000000000000000000..0ca1de3530320c843f623d27d94e54bc332dcc7c GIT binary patch literal 4153300 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%0Avgu)-@n`pQ) z-9-HXun|UwZsyyfj3t7)M7?TVL5Y{UaNryxF)x(Aq7{bcCrZ;U>yS%6q5?57DaSpzk{M+mVo8#dQ6JeuzzwPN(e8 za(yVj%Xzm2VXD?**}oXG>ZKJrK9O#J_;f41H}f^^JLCSy#F{^mj9e^_&Yf$1 zU?=6Nty++Ho@h?rF3p`=nmX0-ORU4BCnlUdFp^#!};ug(%?joa*;>S0;vgedjEl_`~sTa__~P>h3$v%a-fsJl3p9B^aMO z_Yd7)KZYbK_qV^lwy=j1eU-U$@AyUCDGuigATZTb#T;rAy5If2^!eF~p*zzx_fXZ@ zlx?4{p}Q&Lvm5PJ^3|Yws%Q<5?@et|#>lb3vcWe7!LnthxpnJ;p1Yr7M&E=N)noPj zi4laO@mKV=##n`;bMbTctiMKeKI7CjCEE$>?C^>B;y_(wI>ckiV<$^>kq)x@jR48# zyO<%okyDtNHHpu%W28#W+?e3woN{AwG6~ZNRYk5%$0c2c9Bz4h-K*706$+ckZNe&~ za(|*$WhM#T+Ykdw0FcCUFUO3X8L>&;8yD}4=q}YZ)lPW@-C$wRt#>x)27wp4WoFLM zT@<>x%RzK!pj%91=oU)F)dF>?c$rYA8M-OOQ7={qNsDw|p)91E3k3E^6YIejp;lmV z(L4nTUkS&HT)zT<%;skY%1%gm?*(e162N;lH+8CeU%yOu6ioD5CAu%bq&<=tfp-&Ga}6Q> z8_?EQJ-<8n*rr5(wvJ-eYB!IN?pP#UpFH0mHrLf(@%}PpypDQ?v{H;EBZoyQDuVA* zmMq@fg7$##hFodivYn7MPVe2d*Gx0_+f&yIbiLj9#*87GWy{M{akRiaJ4yc@?| zxu(|#>a6WY^_I`5nq@-o%~UOk^rM2sYGXx47KCM$&Di42Z%TOtw4j4 z-g@8BlS^L!7A3M6b`3L=9Ya{zM9&y@Nz%Nz1wRBZ*C^YFJRm@?2C)jbIQ&!?IYDlQ z`a-b9xEn{p&H6-w%RYk7wOcKIWJDb~>Gn0hjo{G9Fi4img^euEKeukvqhJ?2svA}z zWwQc5+hmE`L7vLW5Tz`9EpfrJqGWrtp}lS&@je&?LOG}1#KAz{L>zcc^dd?vQi1A%4w<_ zn&w!ke|gFEdRS)m-neoTMa|kpMkbt*@ zkW*XSP_t>iXoltmZP0w(;DbA}WN5vT=Hrh!B58K}4p@<&PR4rx;d*+LJwmg3HO-rc z3gA5w2;RS4bNrF}GEkLJj0nAmAaju-QIKgAU^*MchyqFlq?Q8G z5_WVQ0i?1mf^2@!*ycdsAPM4|jd8q|yg7hxL}|wd${IDoonDD+To$Gr&kikZ;2EF6 z*(z?{NQTHJ5VIvmgjXDlRWxvlM)<3^SRcY7kj;}C8#o9ufooNt5q!Y-EFpCB)do1o zJ?hJB-|%yMNnh4`f|m)Ldy_tsdHM%jJ6e4SH`^$&ekpjwm0^f!Sc;HO!7S>A-kp_-)DD*w&H$irV~g8lKuW(&z5E*Jv;5WxcE0<|WVW z!Wk8!*x32GSf?;LytQMg8SY1{YZsGCo+ANB{CzF5zjqMXmqYeL5!rYoh3q(FUmiT8 zFJOPCV~iuzuY(Q21_{78tsk=6O~`JwCiTL3E6VyCKixa+DY_JW6F@ zFVUkA5%ETxWf*1oZHckO4IRc;&O}$v*B6>=STffd3#l}5e(=x4xSbl@9(+&#nB77@ zMsME~_V$(fifg=wPJ41r+XMY)rbixlDsGRho&NVZg{Fxk&;P=?$8OsJFWJ~ZTWPcX2|EwP%y6tR2^k((;HMNIMeymr>ZXSE! zDJwpl@Shj0B6TXzS`F(Oj9E7qdjT_R&99l8hg^rW>C>C zG_S04aG+Y+|9Zz6eVO-hYjx14*Baxb&K^tb!KnVF2{Q>k5$p_3QCA=&FCm9jl)aVs zaeqU-T;tlaUM#QMzH+G^JbC^cTYKHBd}rd9TU&1&&#R}afxZ2uK>*oAW^7-cs>*er zaxI6}Zm+}0kCdOwzQt_guT1L#< zGDu~23pN=DVEcQ~S0W;39x3nSfY?GO#%rvb60KoFq_W*%2|&e=Sh%TbVyS`=E4_39IQvImylTb8@&oSRM=GjHYsB%5$J!!Jh+Z*u|ddvoSc;w z{UcWO*;IiYCKB%m0yXzA5mwQJeqC9=S3TzKwR9>=nQCQ*V5C!u9o7v4k550|PFV`$ zxk?ytq9$n+%CscwPWwZ()hF+YB`K+(GD!)Sg;Z&TIBLE6bfqcYjNGL!83S?DMnCji z_@mxRl=($y95=Ceo~Ov?GsO5$leTar>}3H#@fi@u|Hdc8MvasfdLNihLBI2*t=agb zUFG~0?jyO7P?NfSi@Y!br)j#y?v-(*QxEXn#B`F#UdU|_Ibu@=z z9({~yoO*PF*oU6|%QNWz>)3GjYxG7O$B@tO1+YG%4Mew?luZRu=WY?Jo3rnK}PJv&_*n<&*D zSeGDgGo*f_1ckE5HW{0wF|MrWN=c04swZmdx z3DS>08k6LNCm&{;(c=-S-qsDoyC&!NOBKmLfEncA!9!kerPky?zOd0|kX< z3(DE3QNvRi1=rIHvJT<>&BT>vI9j(rF@WR z{I3+K2jfExBz$)1pjU0-Mp55i6N3FOWRQG|Ai5Nc{dV=_Q5(-|>NX#58otZHBlz@h ze_YFxMJ(RuBQJaR(RN$vaLGbVx}Ah{5qx2&uJ|I7c4Z22OnIBehsw9S99T1PDl(8F z4TQr8IxfqRzm0=wC+J3K>5*XtF=-7)} zog(OKoe~ncy_+RmI&h_ELGq2RnRS;4l?lmX2!#x}iwVk>abE#V{b0Hfb6(z|twd5* z0QQF&!lmJw5h|(5HtVR>yr4>Yn;fr0#FnBc)>(&$NiQzxf4uYT(btfKvnfzGqRliofCJX*Q2HFV0 zwc1oJ()RArEld(bPSPApl-8&qRrT{;(JGeP{N72gc*vdwcS2U+A*uYN1qvwy#>`4N zoW}ANOEPZyb|T9sz=&LJ(wAX7gj7FhX+a(MIyUluk(U1!)Xt@$(%JJ5d&i#2JNxeM z=&VzsG2Zv9qNm$yE|s)1?Z=fe_?$>Z8~8Y?w{U{(d|SQ2Y!`eE#qzPyg5kOmTL}-n6VOtHiriA zj>eKQB?LO;_6d5b)ru+7Dp`IFD&-QP zGT2JS%;ogq@xaEiNx&}F&(;4pzUi@1W#@C}kU$Z*5B!w4WVx-@-I=4nC5&_vj$?Ta zvnC#5vOU;YKwBdGFuy1lzps>^rWju>KqZn=kwU)&v&oIXs6$mYd_y?|B40^NY%C~Q zL2{L>ph(cqPV7q`Yr*l9DD|gy{t25NWXCE7@k7>7-#%U12xBZ!i9*JbHpfuR;MXLB5+D^zWKz{q zHobJAR}n)8JygO4gu4g_;;>MW!!Zv#lh9q80uvCafw>uNl;^MmP?=cnd?)xiH3+4I z{(q2A{CDV+?_`d?=Zimi_^Aif@XqcNg-T=FUo6sIBp+3+%eL&vPbA8Jt`kR6ECqf! z79!vNbP?_?{L)jsO>+r1PRrtACh#~mtc%7{QCa^0mHQ~KC7Ln!m!-|T z5-Rx`Ng@(A#@PhgF53e$ryA)R;+ZGlT`T-;SKw0#^rD6Qfh+*WDn(b$w?Txxh zMTr`Xq;x?JKZ!l`+5H>gHiB*s)-Mj2>9TR`&koWqqA=TA@dVuYH#Cb^n(MDomJkjX z9q)!;(4$NvLC6Vy5x=`EP`X-t>v6VN;=Qaekz>TS$V#JB4O2>nlz7_k6aIt-C8xQR z!Q}RY9byr%)@3%y>2QR(Qbr_vEl&==FDd|ci$3Fp&=6iug^%x&BXyJ7iGF%DxFN~X zMCKdpGO15}VZwj8u!|BAZhh}?Us)>u;ZaE%zq_CdKZE~U)!pgDC{3r9^cS zlAW#Bb0!spkTe3npw+-!H`QijUPv1On~M?d+S^REssy`m#9V5_*cQ@1D36az^#a}N z4{n{R7shpsqOqi|)V!KJ*d;rF_k;m37mc>KNr1V8e}})28PiVEij{-9A)6@aZsSX2 zR^3Fr^zjV$H6$FKt+x|O`F8DIJunr!XWwrIdu4OYy%YZ_Vy!B>k)E|8=21-Gr?j2z z{KAPuZ)PmiPI^Oz)>TDdp_)Wno9~wuXEO2?nQ-(bu0>g_((BPNmM?081fvD?n!QO$ z%MB!&;dv)r<*!7g6xBkKyx-2&z3V?Xvd3QE{CIQ!MC-KI{mcj%K*SP2%I41IT-t3( zu#Jl~Y9vZQqXEp^;f z40^w7ESYxFb~;P@?F5!i*UIL32`3TDB%7-(bJ6YOA5k2xlu0=_ zIdF{<-&MoHZGgb9UHIg%u<%Kvu*ymM(*fVIKl=9dB+hHuvl4eZ+onAbY1^=4CAxOD zocT52%&kP;iHtcoDX_79M&z}Uw7|%2ogLF4TaGUbi{s2Hk)qE~r_K~d2}`3OXB?7| zx`_k*dv*7Yy+Ln$Am#cwN^79sQ|jb<&D}7vxY5$f4z%TD=~i>vx9V=$E?G$w+P0|| zhCJ8CCQDbW)6$oyiXEF{1udvs%po?uo>>2xgz{0s&zw7vVF>!CQkkGM~d#L1-AuFL;KBId@;OKm1 z5wW=`De5sg*?JBO6B@PAo+{k;;!u1uLatJ)8O0e!RmE^kc9!X}VF zmaN1;iNqJ&bfQM2f)xh58d?t^wNbwXKC=RG zZBZJh$i|VDV7|NaJwrJh;`J9weZJp6cnM}#Q>U|v;I+wza1TLZhFOn+l|}k zsc~a8zk8(MK1Y81g|W;3=_1%fD@DgN8>B&8D?g$Wa*XM%6BFQ2#6m&0pk;gznOonT zq45^j^Wpk_HoUz7pe&@3qD0LgD>fego%oX}@uIr6A9~N@6r!oS#JSqy%W2&Nxhl0<5L)K zvg}aKZyX&rN;N~5++2ca9L#u6k5&-N4&0Zp1A*{K5UEtz?a?ycr`V%0m#j*#FptzZ z=U4DLi(Ohnv6WVyd8W!Wa3-;5d9tFqof$R z#$w=c%DCu%Ln)h3EkT)L4cF>x^@pGQeeERb+w$59%w#N3bK|c@$Ti*^Zs(*$X?6(D}^@H z?tc2`g0|+l5|nXB=Mi(!?Bp5CIu@}3ioV5QrEMASn|g5XW=Jtw;+Jy|Gb?4vpiufz zXQIjqkW0)vRdd)>{Z;)KV_kfVZ!$|+6%qvz6<%*74>pIzwxj@iBjWc+I922djBw4! zz1WCm6WMY<;O+v2n->@4Ibj*DFXEYwBu%3z`y#cNC$E|nZ`UZiGFzfZB;9LClF#hN zK^u~zfqT9Z<2;nCw_YXwo;mv6$jSGUu#D3mj)ks8M6>~hp3ye~PS<%B)(5@|P2B0x z;Ov*{%fr&uci?Qe8LK!5)>3GMy@cq0^R8N*q+_2joDVIxsPmtUw_^m0wrPN$SAEAS z)G%l(wIH*ox?HaSYTudM8sB0*RiviEIqZ2fgs*YF{#SzGzl!?veuAgh@j6hY5VCxP zFv`Ejx>%!jFeGol@b02ws0WkjBo}pvTxO|7xJL?e^u+ts^i6UQwS0=t(sqFUmn3}D3*7azM8s0U)MLFG=7hQ zcHbcq`;QgqWcDPgT-L3?_z_9 zH069$^BEsIbNl#Xf3Bf0d*{B1$E(bSJAf;}qHH@V+gmPfMmjJ*IaDfRSSvE~t=`m% z%s_c_5%-o)KJ7117x&;~xuPulJF|mW~uJUbv@kNhv`E z9FoxV&o|z_J>5y=?7khT^vydmX$Q;kk_DG7DV2dZ&eiV;-fjGu;aj19Mk&YpsqA?U zbNz8-<9bDcGb!Y2b&poeXUF;HC;XO{`le0g^7!KZk?o_Y*krY6kt-8xPaU2*v~6ssDw&?%SDvU({ePUj z2Vj)-{rLYpw?}p^2^oZyKx7kCP$Nql5UnBzDuPnMS&OT!wAN9z6?Y4c*3l{`TD6K+ z6j5T>n=7>5|dm8dssl0 zjoA?GH!&YP010o$f&))?^%7#pxbXk{+rz))0*JpD;%(ZPWuN@kdV#!GFRaU*g>*5nA}m<28>5 zI@z?2C{=ht)hM;()v8yHL+YCm%3BE-?;r^#+3Ha{c2oN@XmB^ey+FJ~_duv5ZjWdl zDV2*is~a&ji9FaTYO0#1rfUO8HouaLcV`n9_987j#)_GiwV_?x5dbv@qOB=pEKatP z691fPh85Nf}krJ1rDhpxCs}iZHj+Z99XusQ?`&e@(&B`Nuy)_p^U){$iP5-{9sknUTnk=3AE<$pD8cTU(odthV9vn{4y$IDtMLcrXqTSr z*r(z8pju4L<(9T?@?ok`andWCrrxCYiKZ&4QoSoOASI;kJZ?7cFftQG0!g0l%8%~N z?W?YQaM+?dA_MnsYOK;-8T-B^7fixGj@Ydi-u`9F;7OLyc6E5y&!JszsMX`f$~s*d z65sHc93)o0^Y9S9*Wk5IEln8L!JWDS<7vW1F&kr*m^Adi+xLEKCEj7kK|ii@i&Hf5 z`2MRBUzMwngHESyz3q2#9b4Rbx7CO&jC&>S(J#>+(Z{Oqfev;k4{0|kk7>_h{2qj0 zJ5YHFuiu+VzTni4D8rlbzW96FLxGxSwiO=Q*sycwv6@{wx|5IO4|Sh2@zRkaJ&wG* zOE-)j7)Vl!$7f_!j~G3|=}2+z+#B@m*zk4UmHm!v-<;s+K&652^wADzsj0h_at+?D zK){tW2;HnO@@#zxxiW#s2o!*^snM5fZ>je~P*$P6xQ2Ikl0i5R>Uaj(AU0#p(J)Hj z4E$AhUfxx|P_Bb<&9b4FN+Uy{Qv1cfEWX#@WuN(!<*!9xx>*C!P#bk2gr7xC6|uV) z&bz|mL|qTVYw(iti5y4|^a$r|G%UO$>Hy*Ar29g~C6jr*lQ z86kC=GO9EuA^p3}h$xPv-Cwd#Q0ri!WnhosEKbV}U0uxs&O4jJR9R#r7bUrhg35RX zB6{TUBW@&Od6FL;z034ZksjXzORvO$@*WT`LW%N&{tzklGAPtCkO3dUyAbz7Te60P zGs9@Zq;@Si#5Lv1=D}T&_Uc#2Pj0Ovy;g^+(nZ>5@c2?h$xl!$smb{Z7m*T4O-!jn z-HBk*Qb%`0F{SEzJ8Rk!bJDXJIEGe6+*kr)nv#b&**(^troC%)?|s(XO?0mh`BR?Z zz6`#yNsS&T&p0EJ6^?dSh;v?ql5fg;l`iOTC!^=I)HP_7D}xGz{elP?8oF1QqFef; zWBE=&^w9<+i{aa>WF^zG+322P+I^(9y$O@~k9O}0eVbOJjzM3)lGuiy!ePuq8P%mf ziXJ14)Ef5@Pia>A-T(H*-&SDT%U4cphK$&LLYW~vF5 zcNJD z=%y5Rx5*l@o25ixNI28Vy`IcWM=o_zos|3u_rd#(5V53m_}kdg+1Su%3e;MU3k%DV zl&}O6v+KMOyTL>PQ}IIz_X%UG7f*kb!0Z!D_HqMt+;t~u;C{>6yAqLO6O;T0YWzHh z2sMC5wHi5j9ZH&4@$a`Er(1f;F~WISTVxvRZg8RvBz6!a!?XIM-C!0>+%^~wlGGuo zs6HZ~Eqa2*Wl1nnY{&%4K%tkA;SmjhF_Y36+1K(T`@*o0sNd;Z4QsHt`DN?tafxDN zP$_3C9~0!$g;z->62U;dV29PV&UFS=1f!Xa#B8861{KycAF~y@3TxPJ4o!@tT`)gR zHx(2NOiScI=(B?n|G~U`HBk?k4~Ef+vU7c+?3UzQ6zY!7Ds)=2Fh!g~HR!QW4P>G; za(~ioZjOh6iQp=^(Zu8?qUtmp;~y{>O(!xy5{&nMOFavUot&utn{9uQ9&AZ)3yq{5E2LNj4MVJj0rtZNBn zf0Xo(Z!%5}gby3pdS+AUa|^5Pr{oO!Xpe1HS}C=@uXdOdS&w=kbhW0`u6aAbqpI0N zSikJn-SQY)c<=eKE9Bpm;+lHhJNs;As^0<&RN^8*}-_D}fDFxXdeAYpclAU1i^Wb8c z&L8xWTnEN8Yff7WjI-dJG}GZ@GwgLyQ#36%#mI`UMY>z{!NY~>L|Y@RYy{FnNi9Cq zZ%TJ{Vr5E7RNH@G_bydEcwqPLy%OMrFO6x)kb=R(hmROBltZcnp6rf@V?gqZiR{H0QFE8TN+gs;c1H?{MSdgz}fO30Y4eF@IbyyenN-PP4$tsjVJ z9d7*|z6U-)vzTg=S_hbIgf6L_{$PTen4$~R^LFjti&YONgJj96sszkYE!2t?&|#yN zIfsKIz%lH?){AWnw$9fuZTT%?^E#9gCDZ08X8O|WLbp_0XkD~`*q|HueG8hW6#9>Nyg4VUhTD*a&hiR5Q zsAH)jtcc_+PK%9P%TNMX9ExU!tsI5t7Y+$^WAGd!IBgsySufu7vx0)@!&Xkd)cw^H?HBX@{ z-Db91!rkFWqFah(`M(N&8foqJQDV;P3yMRv?hA#9FH3MI*d6GWJs!rH<=h>aw9r1R z2|gfG6x=s3az1qlcF`63We}uG(D>X6^;}Q7uM2RlZPs^os+agt?r+svyc;QLcZ|4v zUGLW%NOsszQf3r8`eOlLx(-su|KGH1***3m4NCC2f~4MN~JH{6|`Z-NOEQ}a=|{n z+UY~A(F7Z`U9n3J!$dS2df0C6?$YrS6N>O7$6}oV!s#dFa;OdSCNM# zyE;W)RO_YHly?r8>uWcK!>tqoBr+@~+nG~iwOC#0X|8aB6+2iamP;v^V>K0}=aHz_ zLPsNka5qWYOuqVB(#S@MyM#xP40(=FIDl>6iTYT`onn&BAe-XM>HfBcD%F&go~hx; zP-t{+y3x4@S#4i~@uYLVr@}FD&ZJp2I_Eb!XNq;MN;-FWF4!VD*0~IqBeOrG*-ZSL zbgqfcMNH&9i8jKRN#~A)bD|k4W%M)x3t43ufoN@=)7;dg>S92VCFT?o-~0FgYs6-3 z4Mb*vs4=(=YG7Av4Bw@XlR%nVFf=4BHMa;_0WJdHR{BYZ@DxEJW0ke;dk6UF*EOjLht; z%nVd(I7y1qjnU|RIG~4r;Eg!#jHQ5xIjf98i5oW&{_id-BT(p`8bW7zrB@iMI(2^|KX3RExV9&KQX*s%5k6pxAURcPg!3Kj+EG42s zzl%k4Jqm$`;j0%aUydZZN#Rk@Bh}vmPNK2vBFb@-(!SX@;i9w5QNn_4$*vr-GE!5` zG?rxE;=7-&j>$1g*niY+rtGgQSfw&~Z+M6EtrpfAT0szqim|R0oN7fBNK{>@C&5@T zoCPjZW@?w4{>jdotGuWEgIYuHqW)Z=f1xN1?XGdnb>Ar=m)Ws%yAP5P_TyT*v3=jz z-;8!gM@6j3B#!A&5^PE&;A+*PHaC4hE5#t|s?TlSx}(*?m%?ows^0FtEEP+5yBV{U z;1>=;Q!u8bCpUl?;IU)=22!(nmi8IZ4o|Y@hilt0=gm5URweU|eB?Kf!TSSH<$h>sKX-{@Z)n&qx$hF9x#FB8rpm zH+9x`jMp74sV8dof zagoM`qpQ<4gs_+aN)!hW5{bgB8Ux1<4|8+bXI^rCv*)~Q?wpE~9#2i30@UFqd-VZz>i`S_Xp9A(V`9pf{u?VR) z4p9^a`v=?@Ry12M($~}6EmWhv@8MT0wbok2)XWNwm?Pw^FqnL&mXmG;)5n!943Lb` z4TQhbBZ%>}Qb&QYdXB{K7{~v0T-$WWIW%hBVpYD`hqiO0(`2g(7oC}{Wi&$vqDpQb z`*^3BO0$LOog*xEiXtir=N10|@~!<~+u~ z3=DlD!!wHUnZ!U9Ku+@s1bbgJ z*yRr5xnZel3z4<3AV^B9#10q?t3}i|N?&^VQOf~ zNl?aJkrZxd+gS!GYUs#y8EkZf8cot`#fP!3Z&r4aEH8O&98|3Gl45JFG8wI1IX+8| zLC+3hV189ucW&7RyW@*ssLgD4n_i_NZM7EkRfUqR!-qN&(z4A3O1^4KNpbdbOtiU^ zGi-_7fguxAx6=k)>+Wi8*;KDM#whEYd-kuy89me%2w?J0CY6qn=keGne3vqGq z#If!$LO)UGQS!H*jXfD({Ew8;YKlG)pRX1Am!&C(A}IsdrXPRaT~yd<-}Sk}G7Rtg zO{orNAfoR4Pb)A&TweqU>lzp#mQTnrnmTFCU73wml@)_)GkSbXdrhn_Co-%!p z9%?=WF_foZ4S9x*-XjpTzJmtJevB#u3lO6@z%Z4Bg~W~y=$aBtZ?je&JVF$QP2R^p zAPF#{qUO+|;gB6aPY&Z|?oUdBlb(&T=Pf)bHbLGGpqz6elVqc(`s?(42TU#HxO4cz zu48+l3LGHjq?FMSEz#t`wE{U7saMM9khTZj&ZHB2$^Q;hSEox8F3@B5L(s^&4P6uO z6KP$jxl9?gA(<&g3KWfV#KLW&?1x03TkX=@?nOEe6N&uUf!LiGe)C)!i&cC+h* zx9ABW=|s35f#?`0n?T47*v95!psd%Mul%r5&G!|XFGVo({~)-OFnW{H(vZB%ANl7$ ztfZpxMc?@lz6Sjm4oIR*VsCS#xLlTNW?g=z5=nB{&EiD^j%DcJZ%|3e=*S_G8VRtUQt#HkS10S)$Q-xm8y9?PQcsHUs-CZ$%a1D?NA(9;nYs`CAvd{HtO-wvy%sL z#t|Eti#QC9jGi&lMu%V!r9Hc+1kd15XvLnLHb_N8 zuigI-d<$i5*(`7AZ*o&3tRK0P`whz&sm!umYd=RlUpN9q&qR;O8j^}2#?e4B6!yC; zsh%~YyVZFZALlAd^vfxs^*HM3`{AVX(ezHdvv6o;1J2Pcb-*{$P7 z-~(f^^^uBW7PG@`cE+NEEsk6g&In?me<-vFeG61`)%vg)M6oxp6JgHKN*Qfi&3JvF zJCPUJ#yj%Mf?4urrd@cS9+#weY8I(^@$c3D&V!ef90L?nN8>9FN4=Ac}#q2BxqsyhqzcZxU`aX9o)%Ynh<;M{0&1hdTso;6WHoVY)= z#vf4wnqKRGh>{;GwgIEAsMDE9DPen_s@k0-1GCHiL_pEof^FSB2~Kdh?MW!J9BN~v zl+l--NGfAwXMrI89QtFL^4bF}%8jMU@wFIx=ds+2{VyX3jNN@Lq^wV6=!Yc1&$+p- z)i*I{N^Pz4c+1vF+W28452{g={(PghR&J=m4WFWLg{E)xOUPjJw7OKkOLZ$Bspr%D zv+5hLjrZ#pYO}PD6&Ipjt-2Sj>T=DiMK69~cC&H^Wer<1l-j#s&pr5pv79f^Y|MV>Szh{j}BA{4+@RP(~KW(G~ ziYJi!>&f)Re`6iSlE}rgT-~I z%;j;rZON%5w;=hP8?o6Y^(x3^8R{@Kw)xRXhcH1BqNa6+!eN*Om@V?ZyC}NIfu2T9 zAStdJC8ZXPRSDhPY=d0`o8ba( z@<#k^8b#E@S~sFQxxo}dJ#qLa5HLeS`$7_VOtDTv2F#5n%^5Pgbc>4-!73nvB5BTi z1ck^m%8?MFx(sddoT(0%Eg^$YS))y>*(J&7Rg1LQ+!8z%(fon-U>6~#{s6gpqmr;U z6z%p-AI&)lEYQL5JB&5Q@X@Fk8%0GZ$(f z=ne(~y`D4d*Sosln^b3#-3e=NvRN?ZNSn+nT4ghZ;LM07lrn6m^f`_agkKBC7i$yq zgdU(xBAnH9^!bi%Vk+8n&1rWrN>;1GL7T)SGqzzkp9DNF)Ks(I-qD^$K3<2-ZIg+` z$0KA;^kD;$V84)Vj&xarPQTsETGdGVAiwdeB>WIFlu1oflEIkx7vuSkpNXa06b>8@ zdnH>kycRr{cuS)JV2q8)lwVXtgUSfyi@VH;B~ua;h^#NEdQVMj8FyKVrRnXb^j8L; zT8^i+jOXzAe_J!9Qq-vqbUjq>$E=yB>tW;8%>PCKe;`lH2vP@>gY)`RA?VvpoATVI z^J_a>Q#JGDGwXLWL?a`Lx;5MJ$bgXXB@(R|Jx|mi_?!xT=rsX(D<*{dTfN<NbM1+9{+Y-!YK8xTeeh@-z`UDQk-)$Ss2} zq08hk36N47nuN_(zE z>q7`u_I^-=$a`)T4v!@l(0x(5D$i!{@YnYzo5%QV4(IvtEZ)OL`OJ6{sYOi z!Ejew(DX&HtFt8-@U}#q+SJgXOKpkP#Dr8=8aXemsi_%hW;1Rq2~Kxvnu#<9!BAIc zG+fmg^tZKj1)5p}4+P<`Bgm;{d~PJbg)pEL#JUCg^Q7Fn235gz#T6~rnRZ6JyU4Q8 zWYM?pC#Hh<%w}&SC2PpUeuitm+d>qhmSXOgl9FgKBVg%pC=8;LG}5}M4N>^mkcQ*6 zsjVH>uo~^cx)nkMH-+6+0td{5^^_89RmjdffcD6%{OX)TTYjYkvg);bnDUOS_C_49 z7%wnmQ{zazc9=|8qC1cvV@N%_KC+@^J;>UXv6ac7-a4#*$Qta%nMl+PQL&Vu+Q7Sw zRgtX@A~@#;{b{M_s}K9WU+p9vpw*o2Q2V8g9aBB4Tcudb zFtuL@lW@DKEr9j0+hwcW-*QIC9@cHnsP1w#hmz08?944p+h;>=kuog_HvEf1nz<`% zwqt3{fUCbB`E(0mo#QM4=^8XHUvVxcp+*7q)q~p@K-PtHtd~eEoa-VUp(O~aiQ}P>AuvbAQSsU?#SQsPH*mxadW?*hqyhiW_ zGu|}*JF$D^9e?Z;rBwV7{!ETI(4N0j@};R}@!cPH`)zHU9GN)L!7FB;O* z_Ra1bv;miISTk$~9M(~GI~fxwm?J6Ih}HwIk?LRf2SPieyJ3~W#DemUrgwH}x+mS? zNJN;U%}cs;a_@8q!o#g?Zb4 zCDV&xS%lXR_rO(M3_h-s;!Mas6x6jdnYzQK8EnMB~RHi%hQA9^)^5lf-D?HHlV%;2LGl>@aPS1C%?L%6LwE@8saC0A4qdqf zRoL5%?Y#NNyNZ={UG|Ox%)H*+S{p-EpsVa)a?V6z?di1<_aurLgB}7WzE9$Rqo#o9 zeAplIA(tE{n%0h~m!lE9PM?ltXeGf#cg;WUyU_e;r>}jZ7PUrv?c1PCC^T4dxq-oX z7m%C}r$wkZLEL82_7bju=ay)?4WK*)dy>R(W@|D+3hBE=IZ01w7>Pr~L7n-W1^v8M zpN2I25w6AyXtP*Z+i7p-Il~rz+figp)Q7}-LxTg{Kn-D@TO+>t= zcL#kTi>bky;5%3w+NJ7Yl5qG@)|)l_n$#q#(_uK&g1+&xyn+xYuX4qm??)%On|Aq* zWk|AaletLj9$46^!C*S3XEjG79iU+NFq4HmRzg>TAtcHZbOou;lAPpkKS5V~ahf83 zV^jsYA}f3WmuLlIa!}k~rFWXPSUp_P_6L9Z-{jGgMN~v*64R4>qO4WoB~{`{)c%8J<;thb;5-bVEq+9w+>h8Zf-W-VZkcZNjtv!+m!)Lp9URltQkNN02WE#`)V4NP75l~N3|yC~ITMp@L}>fd(n9=B zZE3Wc!rk4SE)r_HV6j^d(3@6T!@W9n@ELf^LOW7Z73rjM-~a^KjK`G(iN++-n1=TB znTW}xF)eLOB8|(Vy;BhsD`y#jq_I$%N${S%li18epZjA{s3iA~h0jO;&*>pFr%Rx3 z{z%x&mqw!N?@;QC2ass6B-Qnf6J@w?a+6iY0rtYij15nm&3O;}z)u8JBoo9t@)P^S z_=fzF`Qb3oF&_!e6|8EYL;#+^V8$2aMeOxE^37fi67R@YV6^pAB%~7!I!wgpP|HIK z$A~D2lp`3^Xb{T?8JEJzcHN3rgrGWzI61z8_a#NDbDI+4LL6p~y zn33g(FcB0&gFuQk2$qcKHZ*>;Chr3>qGR-kLKtBI(WUfiP=w|`uxeLayyu5Ghg=)= zquz40D;l*&t4L=l4W?MrM@38p5yebVZeW7KVPrt}Km(9Gp-i-uWW72O?ZzCU1CR#M zL7J@Met%knCc{ZfHUoZBC}_k5UDP%bwK#@FDqkS?3#y%1cKO~e=(0fy428{5i~~|L zuP5Ke`N||^GOFO7YIi@yQWLMAbd7S;Nn=C|P4#v%7gr3)1s|`EBu*ECs(NM4U@4!W zyoe|9%^1QjB@xe3p_T1yX_zq%`pw zeZ`F{zEN)(bmr7(%Zi-FO5w1ziYLGP+HB4UUwf_SIz!3V4q~Cfv$Ezm1j{L8l zt1RugKlY!#Bz9`wQy{z7CHMBeSUG2L$~uan+|%RqCFJ3Bv@0VJkJ^tDz|+SgJ{G5q z0YcQt6C&^==9wpZPK>?>jaN%BTejl9H46qr?q7=aB=)zOZ>2H^g=92&50~6;jKBOZ z_ZrtkB5}A#tCNN4%1zMnvpnDRq>i0MUB2t}+eiWh;bA{rLjmh=Fnm1ED@DJJ9Hfsc z6Vzh;0p$xa<7$LFE3_-7;AgR6J+9x%s(ck_&!ZMv3YsS-%1a7oJ+&*SB-ey%4^X=cW0-V5D zh|+KrP+y^5;tu60H3KE|MFg38fV7%1lo7G%DU4;Yz5s*VLwHm~nCnuY%w+}nnV(|r zev%pi@8QM0PWxD2ppRDvV&*EwTDYF?NoJgKevjmDik^drRZ3ciHq%vV6>;JZ0ilT3 zb=?1zc9>PVR4?GJb%f=2FcVApEfCh}%Xqaw9jLyju2uWMZoR6^MN@XR`Z`Zus!mrn z^3Gz;=jqM!+9RAZwXgItu=fmR)dE*}VJ|#!I&&5`pZ_~L?L3TscHI$ockA67l=_toI(Z`47=jvrUn zs^`Io4b)%Mr)ke=FKA(Xrq-d=VEnj|UfihM$R{uASL$z?T2@BV(&kg~A&N4Lori-K znOsP?gtX*icS)+5-mR=6_Fh`~qjLC1TA8QYv=-%8r?m1@TB%+c$wrb)!nTvIcdTmGre(x~t!@7Nt~N3oTM|+dh>L=}36|5S3o7!c<&;)_ z!dOWwKd9y(X(fv_dzonzE!=*xm2vu?f9+Zs@%!h`5Bc>F#pYc$SKBIDvDs2=S7~j; zMW5=CxZrqZg`o1DES1WDEzvxh_RP^i-!`01UDM|ul(XY z77icq*42DRZsH{zCe@{x20nlTM9zmBG$0BX(bqtCg$v@{5K<|3!513e7T9nqfRXmW zGQEK&w*nM?(d5LWD>xFEI9R($4I%l(aIiwFN1s)kcYAd{nCR*nsY$aSA@0p2$GM2!j!0@mZ{5pbc?sKOh_dFs(6FQ zcoSAVK~nIt4+er~3`X(W__^V(hu-5#lH_ zmTY7IeRD7!0qXU#M@@kQ!*s%~W%AeuLEV!YWCbv>%qq$=CV;OPDk_wbl4VC0zKny6 zgrXP#f%KmNT2dQM0+er@3Q*oT}j$P z^ZUtz)m9Rq@J>6#g^9p!CES=Qa+#@BqRGgG%|_=RqdXkm8^nK$wS%$3bON@( zaH3k-gYkju1#|;+@I1v`p+s*^aTNXj@S8_F#i@gU+x9lSeMEhr=3Z-Bdeo_8BuYL& z#f_hXsoNdto2XUPFOO0Y0woUsmHfr_qqUW7FQ$bKHj?$T zp6>~hh0|k;#WuV!2V6nKnfx+peJb5#>7y7c7?@?ie^&l?8B%DhB4#|Jmq-di*JEXW zVlrn;IyhbKjN$U1o}~4Hy?azHvUr~`q{}x?E?x<(`W6=)Lia-oJjIN zz-#~QZ!frhcHU*JciNP5Qrfc zkB=KwSHJO!!G)K+H+=n(s}CF)eITpl^Yp=k2RD4*kVX2gMx?1N@J65O_vx?k`X1xD z5S`E#)Twfw$tU;8Z@JFB>d<#rU6`_^;Jmlad+YPh3qC(H>9s=_UU=b_EnChc9QIKh z0v929NTH@3NE(YlL37ZK&LL&m9OKO4lefwBW?bi=@#W^3;mJq89s1RQ@mA|ZYtGlR zY6)DftEsDT7ZDvrnCJ|^Y%3ZynIY`i!rayiD7z5h=&xU@rGtU|oXxFIW zu=!MI*c0AHwgDQIOgG}fJBQJ`OIZlKMEw=|^*&PITPLA)1MTJ$(B4!D7Od-}Z+}D{ zX(nFuNj#NzB8Ppf7chp8lIw6Y?#6;1so74_eKU;inu zeDzh~S3{rJxqi^3Nq-!$_qRU^?Pox{$$)0-fkr}-U4%e|dPDo}1hnVl&@M%Sokm2* zD{$WTA>m%DFC^b$Kc4wvWU8^ooMszyIs%EhSl@b4!w(zgxX+rl?flmpuJt)5`SK6v zHM-s1)%Df+WHecZZ2Az;_V68Xg_3)RJ7 z_GqQZ;pObe{R*>KiAh^ z+%fc;Cyr!iXRk?0k}MCakhOUn`mlgDWB-Sxv>v`k_yecm(bybvB ztoB!9|6kLCoGXIim zPVjC#kDRQX7`2V(%}g@%aD7f>z?8iIarx*p`5Fw-i2U;y3|cpOq{m5@;3P)jQc}r3 zpzqV4A;9)3{Z7nG&!|tUuk4xB`BZ32_<_xR&dl`Ao7va%?CbA+^Ww3wpMCd}^L%~v zR~g0e%HNfNn6MV0VOx$4=~aedgEolV5Wmp-a{U5oy<9vod|g97_(`OC>gC;*D|W*0 zjNB55taw*@Pbk>DnFI{oz@&bKel3w*odhS;2{k(ltlKcThLamtdrC@?p zMAB6&rKJiXU5YDTDbM$NRxwX5zss`(2pk3k*>ik{Z|0+IkVz(Ohq1m(*(@)kRY50c z9MyDTH7Qcy7mMUKnvbRT9U~&oYg}iNBUik_6>t#w&Y*9Xp>h0{RGr4jyK+84(ilY< zq>#|MvXY-HUJM%aNdpJ;x1atZ^EUw#pP2xNhtRA%$ULkD&)5GERS%c7M))53zlp zLz8-s{ytD&W~Xt26Bg0ZLzC(z*ACgaA$P#nDI>obIr4(D!*y*tcW(INi!UxX=Yk8s z!T(@g9-^+mP`JIb^^Igzz6khZdBzQ_yvMl4^X^7+8r(Cl|GaNa$*-T^^u+lQyoLFtKjKt;B0z$&Db7`hl0c%V1O#K8S>Y^Dcf7Q zdfYD0AlHn_8J-!tX3QAm8nN0qM$7;JNziU)LLLIS`a!BC+F?>n-;WxmOeUShE{oBH zZEj=5GOx#5wz$aa0kgV_i|2xr%S+2D)_IG(t5luj>Jl`Lx_%v5 zt6yd>+`!D-pe$0Y%6e}FhhF3kyOJTc~qcFdfk;&k}?&p&Q04)J*K7D#fP0EM( zl?Z}l#rccnqaxRgYG#l@ETc7i;`FcJRpu)<>(A(aA$?*i{L@O#o3Oz>%HH}7c_p8g z>vMZ><2x&Z3kvSOcK+Zyhs~dV(Qgjy`Do?JPafsGNUHWNHKzUtz>a1iTuQacIRp-M z!8(SdZU@GgpF=K=;&GP;4n3>pZ+@PuW_#NYD_u3CFS-7#YENE4UjD`!=&9^^k#J(x z(M;wl$Dph+zDo4V7oDK1vFh3)#m0BM@MKcYAjX%)_q*6vM;X*LrgxMbAJbmnM?UAP zVMvC+>uB)Yzb9{b4ii3~fIJiU+`;uM7(!_I%ma6395od@Fr@o3ZOHf9Ra(HY{d3b> zWff(sDk_RxbD5jBVVE{5x9BN6e-S;*X4;dr@b*kA7Tdm)B;6*{b^Ap27gV$Q%g^EC--f2wSQDSr&2 z{TimCUt(6;%5`0nzkGUkX)vwsZ_Z(d?{VgAtvj0fSa;pnx+lLId*0Sxf-vsl5vGBK z7cnZ+asRzd|E+!**k~sk_E)%W4PsZ8uD-_i$v;}w zzWK%zl?UG_yk?^QG^qAI{30iSI0A?X-!Rk~LZu6n$pY$^CbNm#QTxtTZVDCpc9&VB z?%~&$Oc*w|q2~oq-=G$cU5({A2c~5p9~Wr{L~*;`hs4G7?6uNiyVxY z!pIzrF*8a(HB0H|aP_Ig!I&5W-7NOyezrp}H0n`)j`r)PgTgjZg<&}>Yndq4xK#R; ztA7YLw-_5HkH-UE&&*!$oSEFX(PvXN?|4-kKfc{z+V9!8F*&d%S<)C?Oo(n9ftG(~ zX+J1n3{XyH^A8aD2bo#;R~E+tcK!l)mbXJX z>wWwB9drGXZR}-bihdZRu@uwaO{}c}m`>Nj{taS#{+06`z*0U_zgSsBTMj)vK+huZjAl5;EOz2kNO>!fMyYMxvH&wOxK(J zlY4=xIt|nSeHy&lIu^uSIE!WK6#}Zi0cvgG4@at8n*nuELjJY>?s1{la-5(f4@gZ^ zJxMSkgAF!jGzcli=u6yaEG>QjMqDnM^^qPRxG!^`RXtt!JA0@BJw0%q4dxB;7T*x@bo0 zC0!lIpZ-(k9m-1vpi3by0MZKq^foC3A7QQ@BA>+}2;x9IdI}~MmG7Nb&{9=s+PC4O zL#EQn2M$>VSKoP^cFEv^f{1p&bP~|AlYpKFz1;zz#WUXw^wTVlSNs zpt(HqA&~8K0A2Fm0o{wy{C8}{dmy;8F=U(tr+p0em-ur;pgp91Bf+5Fcj9~=ZCJ|> zZKlw&f9@GrI=ORC)K)$6mt$uo4jX2!?aU=r&Jy$^kKjQ1JECGIGSzF1GgVp3I%(ya z#xgu}(vc07Q#|L4`8;>`Szd4QFUNSjt?f@yPXAK+A7uBVLEJaS^U-f9BDa z^2yayRvrm=zeKv0tzdFCBHn(BDjoX!*<@aPoBx|R14f>o5j~M|+Od@_Sa&4aCHz zg0hyP*nWvCgEgOlSa&{WlYYa(aUVNw8*fu;zgZJ}bz{qviLY*KI=Ajl-@pY6#-E=t z<@xp8ein&@F4dQ*38bYH8|q?Xi|l5L{E0H&%h)2r_3?Kad+qb8!tMJu9E$C=;a7Rs zYXyS~$Xq#eD8f>@AvTG@)@`suf-_Z}-siK+Ty43sFxP>oS}Vkb+g5+a+=?|cZ~AcTNh)FBY$d~mLp-CbsV=gac^*~OD9 zt6jUsRdbA(yg0wQG(X>uV#-tP%`Xx{&GxN;WG_H+$~V-S2>H;pxv*z`m}q_%q1nkV zsVG}kUA}aNe^o_!>0)FGR}mf$9%XL6*HZzB+mA@S73}ZBA9o0jD9a(lV@ZkpfPN*a z4>_-PyFYX9KRRTH^D?K?H{|MjhPcn)->fcw^MhmEZ|;9{NIF#30wMYwDir{r{W-?wdcWcKVs4_n@WUw9dIX%L9&NyhtrgrX&IVP7)BI0?Z@DO^Pr zlaUkZXLw66b5|zwX63_++uKDyqTscM$6m5B43zgc!u~T@_okxfLomiLmjI*)8FTK1 zfBK2OH891Q;jJ$8bzkf0vvW{+?YGt2la^N&@94}uKcQgkguz{hh-CjG+~)`=^_dX* z!^Akeijx8b1(6qiha$v*d&c#*XYW5(Su@jzR^;1fm3~vBw`X*bTk=ygXJTM#=hRLT zb#7&rYv4offlLjDLz#ms)?>I#O=HYP!DDjeU9xA&w2#t+uoD)WUGwK_ZVnToWgUtP;pZyC?GFil(g;>?wV3MYSgGcEfwN1+$n(_WVQ$o|x zz~@chW~`5HZ_DVH@#iU9=QNGH_4ZfCt|!0TGzh^cMjdfsf-P7c8(Ul5 zn%P&Gtae%Y4olOlHK}Qu-r8=a=mIf{$$AT!YDvwmJblh5v9Is{X@zyOi89af^(wx; z%d`7?N=^9s4s&S!2KjoBuczBH-Mzj}R%-*F38r!yN>|~cdT`npgbv}K?l9qRF`3}MxSBQYgd%EMF|vso37p7|s6?*5j(4ADZxn-!&j<`gZl(d(x z-n6K_xOh-8$EIE7Pa*E-bIHLw)uNJ*fhmrIf#(-6R_rvg%>Yv*VBT5W1IG0lUq1n+ zthl7O%nNhOMX%qdbc|mb ziS{2-9l0d%nb~8m+Mmz)ad^m%^yyZx-yhl6f1%W~4`b7F2sZ0cM3zDYCh5N=z3|h@ zAHP0QaQNW0gEK<|zaJP%95{MI{no~u!#4Zz&cmJ89?M8iT+tUWe*(pAcZWl0vgO&dd1`Zkt`%`AT02b^H`c{af^jp}id*O1ALobdO z?H%1`=&`dy$B$WrN$y1;!bt(y?Vkxg#<0}FPu;= z(h^i26<4iM$2In*Xa`np0Lky;%7@;qTA$h{L>L8 z(9t^Udz38^p#i$}0>MYb1c`Hn68<9mMf;BM7xI7dDC-4s8DO@b1T$D4{Xc;D^7p;L zT>SIN4=5|pvl}Oxc;oDoY0^DMt1`~pS2HZ@u;S^L)YMYrJ=*c6=kOE6&Gfi*fdd$$%BYnm4*v9Ai6q65J!$GKH zA*SLZP&VV_wVc_?uUgcD-3J}XQ_g>&*Ki&mlkEAfdPJBFwj`kVLbGq&A&D+p4e8g;CLUpah#?oMUurO|evZm^{rFT!;wtitw*%_DS>~HK>S9igbGwbS7 zzgXXxu{kFvC8w1mH8uAHjFJBagoc;+3!5tgYiq>~^rKQ;m?e<{sAe`zhTpLDUfPNmM z@q32bgE&3Rgm0XgQ8(h~S2xtvrq3++u6{~R^2qW1a&lUBZcNWwTUXcKp4K-lt)=A{ zv-v4b08=sDJPs%J6xT9Ct#da#!ri&9soU0HKkBp+^sif~Wu(?*rqU6EJ{q0>)z8h8O3$E*BWP&ggHz*imZ0`1r$P(_}Z+> zuyx0d+0zH}Ebn}L!2EIVevCjFG6jo9Q27%3>epQB;g6>v3@iG?BH!SeFBY513nmWv zyv41BXV1$cN8vlK2a3`OIwrJ{v?qj5oeBd8Ctys0P)s=uhIR@J$s;H*%)MZ|Am8tS zq5k|d7?*3kU~Cn{*8^i2ntdEbc$O#OvOj`Lz65pNMLf%$MZQbrSymNf#hzuvyu7`d zdDrWK8Rrc(jo?`dqX*3u^g|rx9|FcxMlV7DifJ^H&i++BfA_HJ;waY7O}(WqTNtaWVV$G6vS9klT5_v?maXJ2y5 zIoa8{AH87ivtDpmEk|x{E~cu_^~>O_d{D9vVki0)G%PSfm(9w%VCUM~oNpY+X?y6{ zi0(igR z-q>H#`mk2yPn|k-l%K-8zFC>yyIkhh$Wfx>EH=3L*OAj7K}sKJNa-&jbc=j`Lrg;d zg^1|S*^0jTy~ya9QzmZQ*pyb6deK&7bcFPrjP>8v!D9;@5xH3m6e(bmm{Z&sq%z?{ z?>Fd3+(9emCL%5-ak;uje;4-=?V%qcUB|i6m|61y{a?t`H}UN46byVFIevnECdT=n zVYbIm|3y|~e|_?vDSx{{Rj2%Y_XTL6|Gxfb!{K9e{bzFV8i(39!MaytCAybP_iv&{ zF45mW@p}hM*aylv8wa2Hfj<7g_%IB(tE zokZ-|v16+5n5t5I#@)BC+=zbTEu`S3Jk2M_byj5RY*LQyVy-af^~1tb;W;XGSC$+- zno=`;GupB(ZEe<+lyJv!%vv)jG2}q=k;lq;62ATrH0^HG-vg*6?olo+TJOEgCk`KcYR09?uLeecS$Oy8Uk#l0#No9M)}qZ;aNu|qvwJQa@0G}?4vNYJ`Im4TRm&>9{5-p>&?o#=bo(T_he;F=k;Tc!NWg@j`45st{$kt zYviwa9RA+W;45y^KkR-}H|B|fOrk3OzM31KYQYgEpIp4=lm86k;r|)NeK>Lv+(y7q zA!T}>RnEjZ-HtwK9Q)@{^QOTYu(;T|2awB$nj_51IEhJ@W0PFGhU>kXV6k`MnZL({ z@e{qjp00htO6dd_C+Y_+%MR{wMT^hW5~}@I>GLl1myJyP#P+ZJ8LpB#`_7$~!kpt9 zMn=H@Q^|NYi8bOyEGX2owXfi*$>_|PW?f!Uk?$KbE}UFlJt?$cve$oxw$1&%$K`SD zGws{@ZOhp6^TwXf>Ux}F+;3rfJC1H<4hrIdaDIbWAC-E&{ZB{tjXZGX)Wl&g|X3qF>4sBZ-T4!U|#(- z*x}cIqTzG=+Z2DQNwz_imc?hKv<|Ryv=}dXIAC0 zO|K0&oIFur92wX?ZrpX(>A6D;7`5LWgEk3m7KebC^MKG=q0b`i8Mii&j>P^s~qfj)VDbHi|#}N^Iwc#XGW_SqIdj?IOfs2sg zA+*}HZ1o7vn48du8BjH_0s3+9)nxG*KpIe34>tD75_nMW?CZo&;GJzBEt4r_|z~`l`Vs42``9Hw8`*avW&ghBI!E$z}?A9Wl$_wXbe3_$S7~yLI3>xYE zL@D!psCYel6mQwfO35N|k}Co~JxNcBphF;f?#)OoZQjD7;ck{yHLYlaiVP%XZg43OA*rUuNjwRS{1a~4-F_SuJE)dWyIoL2)p3(dz7>ix4)mu z>+(w2p)0U%jlcBbi-DoQDg6#U=OcFTV`z^bV=S&9XVIVZ?Z4i=bG_FKJo0@WRmx-yQw*!gm+G%UgG$`}_s2%`@?(F%yC!xqdIu=b^($=l}HO zA7>s>y@?^`Og%kpr;WkZVDvAl+T5%jXtt^c)C0T~MjLfL6!2EKq_d#~K4jl%%uXkH zrT?nGlilQQ?f-3tyUkklO-9={Rhv4SQa10H(0|^9af!2Unw>Z<@tsYbe`PFx4=o!H zjtgM?J_6@E0h#eJH06`^LyvZittfvn)H!Hwaq(vVnBqg}XYX~qysiAR@`6tbnp58? zXl^|;HT^UgK6L#P@C1I0!0_28V7yt=3&t6h3NW^v3WM-jq=>teHOjTv4Q_c}qGw3k$nm8oU+sE68{337jXc#e&bI>PK8NE?7@2 zeGkyPSlFh+z>6`BR!ko0!FZ?ZvQD@3#hff3gddrQ578rdc&$RF`KvM!6ZbO&a9@XW z|24Iyy=lsYIeqkLO-6LxD{afaJ52gEC1VMZ4bvAky|z^O_!XY=SC}+^qJP0N---lr zJE0H-hQq5%|E1T|dbKj=Eb~A6`IRQoc;}ws+*RT&HWInJ*!-{sMHM(fk3sW$FFe+L z;P`W?MX*n=)8;aw@9D3%{%dchGW*Ii*9NOGB&};yytW-1^YQo0bdk5RytJ`sbz|{5 z0$!d#@jMCn=yu#;RD#(z5QLOUSVx+Y78)PvXW6(nW!puOh$|(M=J1axAJ%4Rv$O3-SoHY8%amzDvOYP7XW=a+x8IJf zAPM7Hlyx}d;d)*He|Hxvat=CUx8Yb7MZ#L3pUu7~q!w)J<`qi+q_cBdOsma!A)rCo@51#CfuS4$@q7%@i4bH_E*ko?*E>* znzYM|hbQHD*~#&Et+a9W>c;B`qd<=Mf|XnVuZ#08F5Tx5bbSbCU8w z+Bn#Y;KI+w54VR2!vi2^S=q=*B|}R}z_&jbGcpwyqe7_Y@6m(yhbMrG*Tbnh+|KZ* zXM*itSvM_hA7?c$nRC;pBR0(@kJh*=&rbc}-5ncN{OcfV;tgcoTd?MR0vG)gthqPi z!?Ts~T%?qw`MVmFnd$w5L9Y>L+UQ%keUtP96))!jOUwU9+IIj%b!G3rH=P;Uz|djn zFo>WaO~tNY)F`oJ6Vjt@Vv26cCMLUUvZnlS*)3XbjeZ9!E{kbnU4wWADZDrsvJh>R7*s30@I+ry8(w)L{O95i|LDy!B~J zk?k`2_EYA2_e`+OcUlwxhm}>#M+n98C9Kd0 zWdP~Z39NwGWKCh7^J~!Rs1@n*a?mo*PzMiDaVf*^ix*R?RyePq9?4sEYQ_h9k`Ka3 z=qlWqrbEJ9i+5}~v=(nc;*4gRvDFPo<!7qlh+fT3j)+VNNHX<9IH5;2ocq*mkY-!-p6rYMe1*L`2$9`|EhZGGH=`zM^Q z@+}SvsS7EdxIL_6eRlcNwFpY_4>&%-B@CA6K#Ya^A&=Ji*J-G6JMhqxlaQ-Y`TPpt?%a?0VWL(sc8kH~q1QAOrFlEanEcQY`jLNXg8 z<&CtF0~kL?o_(JIz%7VES*`!cfEr0V1li^SSBsWNIOg~c}uAPQUZcx*2-Al`tn0G8(_CPkYn zSLoJ2z{?Tqc4~FHojO%~&c0NY7z|!rsytO8Q^xD|k)RU$Fb*!UZ^7HpoXvz|7Z)VM zSC}WOchxN523Ur@ng*@RCiensz=y&wJ=V_ZO{{vZ`IYWhngjkF>HcBB_$sjEINn)> z>#&#m*eU{lw+bh`i*G|BkzvjmX+;m)3PF>XWNZ}yzgtB#TSY%!gnLG-;ILKr7I~c> zTSfYBs+etOOW_@U2l7=ZG|h7{=e~fI=x>B)Wn}45LLt~uLZS0#{U4us(r7deyfE-W z_X`8=pE(_C36UCFY zkKF@XTDh(~GnPJMK%WWU$F7`46a;!>H+wX z?3XZ!Gx=8?&KDqUXV2ddakO;5|E0M(?RdB3e=Vo{KnX z+=ChGwry>cYc%Sb@%vg)65wjoMT2NX?Uv757Ek1gkznr0!xN$tX>&NcUGD#&>)Jk9z039IS3dSEEkMQ@GuAf?EHC#! zd*=^@X(w7}@#|HIuy9qtJrA3M zu;RCZRa6c>(G2WV-^N>zgmwHK=EbazHP%u-uYOL%C+7CvuBcA)KR5#g^yi6-`93yh zH=A?epE&~wU-!VZw1k-l``$L}p1r|Hd>?DB2D^j3XR7|f&F2oaP4)iKYOQPy>$AQ_ z3A(55o_imcaJnc{c{(*PajyYf&l7N}+yohDH|?8TQ}TZa)453R*A>`|hzBIH8XRmCy| zwyG&@8^x2!!nBAx>=_@IAxH_6X_UoTM@@aT+O|^h?yOkd;~BA#={I6+hxY`uLN8-d z+zg6@7!%}MI;BjVkt0^bi&nU_un^jpkDy>`#&%>SK_)HaccNFBoFOj4- z3ek>fwBilySWQ#I>HU)L~k}1uh+ITZtgA%&} z6}TExP(v2p%9n6gZ6h^ke^Y}J4kx#3@IkB5cGhL&kWB?i!HgNM0PA#xZ{_0_ecAab zyskcBjBbAn9m+Qr$mYAm9qq64Nf$~JMbW(lrAKAjG%G~eE zT~a?qG<^TGpYjccdoRqJXfU{L`DM1c4>}>9C?rCoB`SDnQ+k;Epp{EN(oY4m% z7u>-3B*ExGlEwaYV_17+&I8=Cr9q{qt=6ED1%a^JecU+dOjiz@ua~(WqrhH~ zf_@0YGd;w_V=9tiGWrIT@K++(e%L)FsG-j+BmEG=?=28NQ0A=f*IJ))l+JRC?%h*e zfDlvcd^EZ*a)eBLn-w+rjy=(M8^1TSMx*;8k{N!yeP+}(*zT5Zx6%-s&-;*ABk-hN zg@&~R@}@KH*@SkxRSr4~aFQlvb5_|VPdYORYTAaZ4>_D0H#osPTuv}2*w$cca6?)g zup6;{5a%HKI+BKQNpvu3l`0+qrqntmc0*eAPD-gJvU@xQA|zYZt8>&_)Hz$Qqbk8} z>V53aKZn3Mko6W(9){BH0O5mwbFuJ^#@Q#sjYf}CH{69n2VvJe_Tq8M7#CEK4(m2)N3?K9y3?CeX#dj09gJIC+?L?0T zFh65{hm$PzTD-DW>=YlN6=pBk{?+*%%ICe&|qLb$?IY z{=)}DAL!{1oj(U0#wzS570_5ci?+`}D)~}~CKsrPWI>NFmE=>;GdCm5whyJ9Hl*Dj z&_&6bqpmq#>#05)FdVmFn%jK#E|x=EK>8sDTIzpcwI=;=FqQ2GLBBwIJ)GI6CH)|a zf5N*aTcr3>fPV0*%9?upr9MBlhQEO0DuA|W7T$#Ku>&IgusW6P2T}i^w%uR5PfPki z79V41x#DiF4@5tR)cvtT(UlzN8llmI6u$*KNaBNa9{nN5F3y_jo2qCx?c0~#=Hru- zljGxKl?v7q%Qd&xUf^6Jv?8wU2iW<1%kF%N@jiH<7vIC!PG+{x8OHm}tq(kd_vk}rB{;3$BcPHG#I4f^e3A-l>|dAn z^m}@_na|~E3Oe~;|Js=tMFk}#PKquVmRBl@sE>|&D5X~ebHb=vjKwfYiqI~9gT$~2 z(oPAUMmH6;URRqkMUisAa=fU>^{u0ojJ{)l8}(Wg?cnX;%{W)K(E~@nrw4rhKWmVL z`QpwUSt(BfNy)xtdph55tmdZho~fjd+8S47NrV82Xx1t}P=Z+n))2FOf*GrjwGY1>0uyAg1&?3dVBSKkVshE)ST70&I^&L?0) z6l0T+kGbe=<}DktMKd>$QSt`YvRY_MX`Ni>Q@S%Z*KjTAMs9BR4NL5gDLC9GyHAds z=3aHJyXqR0Ih&yzBQ*AxupA7a`DHLkmN_1hFtp3CsLUww@9$4?;Fqi_Bn8VLd9tj&l=k;|O&h z9ZlWmetrJLb99^A{yrZsMBVWe_oIUQoHZm)6HOms3wCJux=RI^2-C(@;5M#;o*w#0 zPd2Im+AR&;_AxYtI?P^Ku#Sb{7NC`*7RxTnqORz*&ZUFyeGWnWUgBMm4i2kaI{8&U zUT)MMY&=%=i9YhBfSJ!>QQrn(s2^JmJybAXRpefh=_8} zBSR4pwbP$G`%pEGu&ZpTonBiz9Twb62&m`|OWj)7lS`21Q~}vAKKktyZY@=nvmP>N zq8uXqoBEBBP10edgIe!V71-<1E9s>ZrNb3chOJ=zNCj~D{NF0rI8wnvNZS8X3rGbL z+`>X^cJ70cu7El6n_Ecyqg&WF(gI)bkN>9@q@e=Du3%obDIusI%hjR;nrGoX<}M#SGms=-T)mxWs22rg0`%^y{IUli`!~z?d|Z;bNnzV zlS!Ny1?(a*#X#&0_W-6gKqlXb3i9kuN{FwcMFsz`mBYd4<%1Vg@>O#b{0y`prcQ%lm2R^yolKOw=@m2KahNOZJ*(m zsv)F*D<+K>iMjx4&nxJAA$__U4B2SmoNUz!#WFzIFGmY&VcRo|9;{LAi=-6f_; zk53Z(X-NncP(qwaA=m#ZfG?cKxWn%DBqa3R7(lCR`B+Y=*DvO%56pIcQ6ZKO^sV>{>9ru)rH|2>ZRC;e zq|~t+UB&V>5a2O!*J3MKf>_RoISk_OYlJ|S-~~@=nvvd*y$cS^;Gy{OJ(

;(oo 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

jr{s{lV=4d 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