diff --git a/.claude/CLAUDE.md b/.claude/CLAUDE.md index faf4dc5c8c..f7f572e3cf 100644 --- a/.claude/CLAUDE.md +++ b/.claude/CLAUDE.md @@ -31,7 +31,7 @@ Generic Flutter and Dart standards live in `.claude/rules/`: ## Project Rules - Most app work happens in `mobile/`. Key entry points are `mobile/lib/main.dart` and `mobile/lib/router/app_router.dart`. -- Use current code plus focused docs as source of truth: `docs/STATE_MANAGEMENT.md`, `docs/BLOC_UI_MIGRATION_PRD.md`, `docs/NOSTR_EVENT_TYPES.md`, `mobile/docs/NOSTR_VIDEO_EVENTS.md`, `mobile/docs/DESIGN_SYSTEM_COMPONENTS.md`, `mobile/docs/GOLDEN_TESTING_GUIDE.md`, and `mobile/docs/PEOPLE_SEARCH.md`. +- Use current code plus focused docs as source of truth: `docs/STATE_MANAGEMENT.md`, `docs/BLOC_UI_MIGRATION_PRD.md`, `docs/NOSTR_EVENT_TYPES.md`, `mobile/docs/NOSTR_VIDEO_EVENTS.md`, `mobile/docs/DESIGN_SYSTEM_COMPONENTS.md`, `mobile/docs/GOLDEN_TESTING_GUIDE.md`, `mobile/docs/PEOPLE_SEARCH.md`, and `mobile/docs/SHOREBIRD_CODE_PUSH.md`. - Older docs can drift. If documentation disagrees with code, trust the current implementation, targeted tests, and the newest focused doc. ## Architecture And State diff --git a/.github/ci-timing-budgets.json b/.github/ci-timing-budgets.json index 9071341dce..37ec1c58ad 100644 --- a/.github/ci-timing-budgets.json +++ b/.github/ci-timing-budgets.json @@ -24,6 +24,7 @@ "jobs": { "Detect App CI Scope": { "warn": 40, "fail": 90 }, "Codemagic Variable Groups": { "warn": 30, "fail": 90 }, + "CI Configuration Tests": { "warn": 30, "fail": 90 }, "Tests": { "warn": 600, "fail": 780 }, "Generated Files": { "warn": 260, "fail": 420 }, "Analyze": { "warn": 150, "fail": 300 }, diff --git a/.github/scripts/tests/test_codemagic_android_build_number.py b/.github/scripts/tests/test_codemagic_android_build_number.py index 4f31a56091..009b7d5361 100644 --- a/.github/scripts/tests/test_codemagic_android_build_number.py +++ b/.github/scripts/tests/test_codemagic_android_build_number.py @@ -1,28 +1,258 @@ +import re +import subprocess import unittest from pathlib import Path CODEMAGIC_PATH = Path(__file__).resolve().parents[3] / "codemagic.yaml" +MISE_PATH = Path(__file__).resolve().parents[3] / "mobile" / "mise.toml" +MOBILE_CI_PATH = ( + Path(__file__).resolve().parents[3] / ".github" / "workflows" / "mobile_ci.yaml" +) +WORKFLOWS_PATH = Path(__file__).resolve().parents[2] / "workflows" +SHOREBIRD_DOC_PATH = ( + Path(__file__).resolve().parents[3] / "mobile" / "docs" / "SHOREBIRD_CODE_PUSH.md" +) class CodemagicAndroidBuildNumberTest(unittest.TestCase): - def test_android_aab_build_uses_google_play_floor(self) -> None: - contents = CODEMAGIC_PATH.read_text() + def setUp(self) -> None: + self.contents = CODEMAGIC_PATH.read_text() + self.mobile_ci_contents = MOBILE_CI_PATH.read_text() + self.mise_contents = MISE_PATH.read_text() + self.shorebird_doc_contents = SHOREBIRD_DOC_PATH.read_text() + + def test_codemagic_yaml_parses_with_aliases(self) -> None: + result = subprocess.run( + [ + "ruby", + "-e", + ( + "require 'yaml'; " + "data = YAML.safe_load(File.read(ARGV[0]), aliases: true); " + "abort 'missing workflows' unless data.fetch('workflows').is_a?(Hash); " + "abort 'missing definitions' unless data.fetch('definitions').is_a?(Hash)" + ), + str(CODEMAGIC_PATH), + ], + check=False, + text=True, + capture_output=True, + ) + self.assertEqual(result.returncode, 0, result.stderr) + + def test_android_aab_build_uses_google_play_floor(self) -> None: self.assertIn( 'google-play get-latest-build-number --package-name "co.openvine.app"', - contents, + self.contents, ) - self.assertIn("BUILD_NUMBER=$PROJECT_BUILD_NUMBER", contents) + self.assertIn("BUILD_NUMBER=$PROJECT_BUILD_NUMBER", self.contents) self.assertIn( 'NEXT_PLAY_BUILD_NUMBER=$((LATEST_GOOGLE_PLAY_BUILD_NUMBER + 1))', - contents, + self.contents, ) self.assertIn( 'if [ "$NEXT_PLAY_BUILD_NUMBER" -gt "$BUILD_NUMBER" ]; then', - contents, + self.contents, + ) + self.assertNotIn("PROJECT_BUILD_NUMBER + 8", self.contents) + + def test_shorebird_supporters_defines_are_optional(self) -> None: + self.assertIn("optional_names = %w[", self.contents) + self.assertIn("SUPPORTERS_API_BASE_URL", self.contents) + self.assertIn("FF_DIVINE_SUPPORTERS", self.contents) + self.assertIn("defines[name] = ENV.fetch(name, '')", self.contents) + + def test_shorebird_required_defines_reject_empty_values(self) -> None: + self.assertIn( + "required_names.reject { |name| ENV.key?(name) && !ENV.fetch(name).empty? }", + self.contents, + ) + + def test_shorebird_install_reuses_only_the_expected_cached_version(self) -> None: + self.assertIn('EXPECTED_SHOREBIRD_VERSION="Shorebird 1.6.117"', self.contents) + self.assertIn('if [ -x "$SHOREBIRD_BIN" ]; then', self.contents) + self.assertIn("bash -s -- --force", self.contents) + self.assertRegex( + self.contents, + r"(?s)&shorebird_install.*?script: \|\n\s+set -euo pipefail\n", + ) + + def test_shorebird_release_commands_are_signed_and_preflighted(self) -> None: + self.assertIn("*preflight_shorebird_ios_release", self.contents) + self.assertIn("*preflight_shorebird_android_release", self.contents) + self.assertIn("shorebird releases list --platform=android --json", self.contents) + self.assertIn("shorebird releases list --platform=ios --json", self.contents) + self.assertEqual(2, self.contents.count("shorebird_release_preflight.rb")) + self.assertNotIn("shorebird releases info", self.contents) + self.assertIn("--build-name=$BUILD_NAME", self.contents) + self.assertIn("--public-key-path=build/shorebird/patch_public_key.pem", self.contents) + + def test_ios_release_rejects_closed_app_store_version_before_shorebird(self) -> None: + workflow = self._workflow_block("ios-build") + self.assertIn( + "app-store-connect get-latest-app-store-build-number", + self.contents, + ) + self.assertIn("--include-version --json", self.contents) + self.assertEqual(1, self.contents.count("ios_store_version_preflight.rb")) + self.assertLess( + workflow.index("- *preflight_shorebird_ios_release"), + workflow.index("- *build_ios"), + ) + + def test_flutter_pins_match_current_shorebird_engine(self) -> None: + mise_version = re.search(r'^flutter = "([^"]+)"$', self.mise_contents, re.MULTILINE) + self.assertIsNotNone(mise_version) + expected = mise_version.group(1) + + mobile_ci_versions = set( + re.findall(r'flutter-version: "([^"]+)"', self.mobile_ci_contents) + ) + codemagic_flutter_versions = set( + re.findall(r"^\s+flutter: ([0-9]+\.[0-9]+\.[0-9]+)$", self.contents, re.MULTILINE) ) - self.assertNotIn("PROJECT_BUILD_NUMBER + 8", contents) + shorebird_release_versions = set( + re.findall(r"^\s+FLUTTER_VERSION: ([0-9]+\.[0-9]+\.[0-9]+)$", self.contents, re.MULTILINE) + ) + + self.assertEqual({expected}, mobile_ci_versions) + self.assertEqual({expected}, codemagic_flutter_versions) + self.assertEqual({expected}, shorebird_release_versions) + stale_workflows = [] + for workflow in WORKFLOWS_PATH.glob("*.y*ml"): + workflow_contents = workflow.read_text() + uses_flutter = re.search( + r"flutter_package\.yml|subosito/flutter-action|" + r"\bflutter\s+(?:analyze|build|drive|pub|test)\b", + workflow_contents, + ) + if uses_flutter is None: + continue + versions = re.findall( + r"flutter(?:-|_)version:\s*[\"']?([^\"'\s]+)", + workflow_contents, + ) + if set(versions) != {expected}: + stale_workflows.append(workflow.name) + self.assertEqual([], stale_workflows) + + def test_shorebird_patch_commands_publish_to_staging_and_are_signed(self) -> None: + self.assertIn( + "shorebird patch android --release-version=${{ inputs.RELEASE_VERSION }} --track=staging", + self.contents, + ) + self.assertIn( + "shorebird patch ios --release-version=${{ inputs.RELEASE_VERSION }} --track=staging", + self.contents, + ) + self.assertIn("--private-key-path=build/shorebird/patch_private_key.pem", self.contents) + self.assertNotIn("--track=stable", self.contents) + + def test_shorebird_patch_workflows_gate_branch_and_release_commit(self) -> None: + self.assertIn('if [ "$CM_BRANCH" != "main" ]; then', self.contents) + self.assertIn("RELEASE_COMMIT:", self.contents) + self.assertIn("git merge-base --is-ancestor", self.contents) + self.assertIn("git diff --name-only", self.contents) + self.assertIn("*prepare_patch_source", self.contents) + + def test_shorebird_patch_workflows_block_native_asset_and_dependency_changes(self) -> None: + self.assertIn("BLOCKED_PATCH_PATHS=$(git diff --name-only", self.contents) + self.assertIn("'android/**'", self.contents) + self.assertIn("'ios/**'", self.contents) + self.assertIn("'assets/**'", self.contents) + self.assertIn("'shaders/**'", self.contents) + self.assertIn("'pubspec.yaml'", self.contents) + self.assertIn("'pubspec.lock'", self.contents) + self.assertIn("'packages/**/android/**'", self.contents) + self.assertIn("'packages/**/assets/**'", self.contents) + self.assertIn("'packages/**/shaders/**'", self.contents) + self.assertIn("'packages/**/darwin/**'", self.contents) + self.assertIn("Cut a normal store release instead of a Shorebird patch.", self.contents) + for command in self._shorebird_patch_commands(): + self.assertNotRegex(command, r"--allow-native-diffs|--allow-asset-diffs") + + def test_store_artifacts_are_built_by_shorebird_release(self) -> None: + self.assertIn("*build_aab", self._workflow_block("android-build")) + self.assertIn("*build_ios", self._workflow_block("ios-build")) + self.assertRegex(self.contents, r"(?m)^\s+shorebird release android ") + self.assertRegex(self.contents, r"(?m)^\s+shorebird release ios ") + self.assertNotRegex(self.contents, r"(?m)^\s+flutter build appbundle ") + self.assertNotRegex(self.contents, r"(?m)^\s+flutter build ipa ") + + def test_release_jobs_never_materialize_patch_private_key(self) -> None: + self.assertIn("*write_shorebird_public_key", self._workflow_block("ios-build")) + self.assertIn("*write_shorebird_public_key", self._workflow_block("android-build")) + self.assertNotIn("*write_shorebird_private_key", self._workflow_block("ios-build")) + self.assertNotIn("*write_shorebird_private_key", self._workflow_block("android-build")) + + def test_shorebird_cache_is_enabled_for_release_and_patch_workflows(self) -> None: + for workflow in ("ios-build", "android-build", "ios-patch", "android-patch"): + self.assertIn("- $HOME/.shorebird", self._workflow_block(workflow)) + + def test_shorebird_install_runs_before_ios_dependency_resolution(self) -> None: + for workflow_name in ("ios-build", "ios-patch"): + workflow = self._workflow_block(workflow_name) + self.assertLess( + workflow.index("- *shorebird_install"), + workflow.index("- *prepare_ios_spm_packages"), + ) + for workflow_name in ("android-build", "android-patch"): + workflow = self._workflow_block(workflow_name) + self.assertLess( + workflow.index("- *shorebird_install"), + workflow.index("- *setup_local_properties"), + ) + + def test_shorebird_release_preflight_runs_before_store_build(self) -> None: + for workflow_name, preflight, build in ( + ("ios-build", "- *preflight_shorebird_ios_release", "- *build_ios"), + ("android-build", "- *preflight_shorebird_android_release", "- *build_aab"), + ): + workflow = self._workflow_block(workflow_name) + self.assertLess(workflow.index(preflight), workflow.index(build)) + + def test_shorebird_release_builds_fail_on_missing_preflight_output(self) -> None: + for anchor in ("build_aab", "build_ios"): + self.assertRegex( + self.contents, + rf"(?s)&{anchor}.*?script: \|\n\s+set -euo pipefail\n", + ) + + def test_runbook_uses_current_patch_promotion_and_reporting(self) -> None: + set_track = ( + "shorebird patches set-track --release --patch " + "--track stable" + ) + self.assertIn(set_track, self.shorebird_doc_contents) + self.assertLess( + self.shorebird_doc_contents.index(set_track), + self.shorebird_doc_contents.index("shorebird patches promote"), + ) + self.assertIn("`shorebird_code_push` is a runtime dependency", self.shorebird_doc_contents) + self.assertIn("Crashlytics", self.shorebird_doc_contents) + + def _workflow_block(self, workflow_name: str) -> str: + start = self.contents.index(f" {workflow_name}:") + next_workflow = re.search(r"^ [a-z0-9-]+:", self.contents[start + 1 :], re.MULTILINE) + if next_workflow is None: + return self.contents[start:] + return self.contents[start : start + 1 + next_workflow.start()] + + def _shorebird_patch_commands(self) -> list[str]: + commands = [] + lines = self.contents.splitlines() + for index, line in enumerate(lines): + if "shorebird patch " not in line: + continue + command_lines = [line] + cursor = index + while command_lines[-1].rstrip().endswith("\\"): + cursor += 1 + command_lines.append(lines[cursor]) + commands.append("\n".join(command_lines)) + return commands if __name__ == "__main__": diff --git a/.github/scripts/tests/test_ios_store_version_preflight.py b/.github/scripts/tests/test_ios_store_version_preflight.py new file mode 100644 index 0000000000..d8d6599c99 --- /dev/null +++ b/.github/scripts/tests/test_ios_store_version_preflight.py @@ -0,0 +1,89 @@ +import json +import subprocess +import tempfile +import unittest +from pathlib import Path + + +SCRIPT_PATH = ( + Path(__file__).resolve().parents[3] + / "mobile" + / "scripts" + / "ios_store_version_preflight.rb" +) + + +class IosStoreVersionPreflightTest(unittest.TestCase): + def run_preflight( + self, + payload: dict | list | str, + candidate_version: str = "1.0.21", + ) -> subprocess.CompletedProcess[str]: + with tempfile.NamedTemporaryFile(mode="w", suffix=".json") as file: + json.dump(payload, file) + file.flush() + return subprocess.run( + ["ruby", str(SCRIPT_PATH), file.name, candidate_version], + check=False, + text=True, + capture_output=True, + ) + + def test_allows_version_above_latest_approved_version(self) -> None: + result = self.run_preflight( + {"buildId": "build-id", "version": "1.0.20", "buildNumber": "848"} + ) + + self.assertEqual(result.returncode, 0, result.stderr) + self.assertIn("newer than approved App Store version 1.0.20", result.stdout) + + def test_blocks_same_version_as_latest_approved_version(self) -> None: + result = self.run_preflight( + {"buildId": "build-id", "version": "1.0.20", "buildNumber": "848"}, + candidate_version="1.0.20", + ) + + self.assertNotEqual(result.returncode, 0) + self.assertIn("must be newer than approved App Store version 1.0.20", result.stderr) + + def test_blocks_version_below_latest_approved_version(self) -> None: + result = self.run_preflight( + {"buildId": "build-id", "version": "1.2.0", "buildNumber": "848"}, + candidate_version="1.1.99", + ) + + self.assertNotEqual(result.returncode, 0) + self.assertIn("must be newer than approved App Store version 1.2.0", result.stderr) + + def test_compares_numeric_components_instead_of_lexically(self) -> None: + result = self.run_preflight( + {"buildId": "build-id", "version": "1.9.9", "buildNumber": "848"}, + candidate_version="1.10.0", + ) + + self.assertEqual(result.returncode, 0, result.stderr) + + def test_fails_closed_on_missing_version(self) -> None: + result = self.run_preflight({"buildId": "build-id", "buildNumber": "848"}) + + self.assertNotEqual(result.returncode, 0) + self.assertIn("missing version", result.stderr) + + def test_fails_closed_on_invalid_json_shape(self) -> None: + result = self.run_preflight([]) + + self.assertNotEqual(result.returncode, 0) + self.assertIn("expected a JSON object", result.stderr) + + def test_fails_closed_on_invalid_version(self) -> None: + result = self.run_preflight( + {"buildId": "build-id", "version": "1.0.20", "buildNumber": "848"}, + candidate_version="next", + ) + + self.assertNotEqual(result.returncode, 0) + self.assertIn("invalid candidate version", result.stderr) + + +if __name__ == "__main__": + unittest.main() diff --git a/.github/scripts/tests/test_shorebird_release_preflight.py b/.github/scripts/tests/test_shorebird_release_preflight.py new file mode 100644 index 0000000000..e9471ed89d --- /dev/null +++ b/.github/scripts/tests/test_shorebird_release_preflight.py @@ -0,0 +1,152 @@ +import json +import subprocess +import tempfile +import unittest +from pathlib import Path + + +SCRIPT_PATH = ( + Path(__file__).resolve().parents[3] + / "mobile" + / "scripts" + / "shorebird_release_preflight.rb" +) + + +class ShorebirdReleasePreflightTest(unittest.TestCase): + def run_preflight( + self, + payload: dict, + platform: str = "ios", + release_version: str = "1.0.20+841", + ) -> subprocess.CompletedProcess[str]: + with tempfile.NamedTemporaryFile(mode="w", suffix=".json") as file: + json.dump(payload, file) + file.flush() + return subprocess.run( + [ + "ruby", + str(SCRIPT_PATH), + file.name, + platform, + release_version, + ], + check=False, + text=True, + capture_output=True, + ) + + def test_allows_missing_release(self) -> None: + result = self.run_preflight( + { + "status": "success", + "data": { + "releases": [ + { + "version": "1.0.20+840", + "platform_statuses": {"ios": "active"}, + } + ] + }, + } + ) + + self.assertEqual(result.returncode, 0, result.stderr) + self.assertIn("No existing Shorebird ios release", result.stdout) + + def test_blocks_active_platform_release(self) -> None: + result = self.run_preflight( + { + "status": "success", + "data": { + "releases": [ + { + "version": "1.0.20+841", + "platform_statuses": {"ios": "active"}, + } + ] + }, + } + ) + + self.assertNotEqual(result.returncode, 0) + self.assertIn("already exists with status active", result.stderr) + + def test_blocks_non_active_platform_release(self) -> None: + result = self.run_preflight( + { + "status": "success", + "data": { + "releases": [ + { + "version": "1.0.20+841", + "platform_statuses": {"ios": "failed"}, + } + ] + }, + } + ) + + self.assertNotEqual(result.returncode, 0) + self.assertIn("already exists with status failed", result.stderr) + + def test_allows_same_version_for_different_platform(self) -> None: + result = self.run_preflight( + { + "status": "success", + "data": { + "releases": [ + { + "version": "1.0.20+841", + "platform_statuses": {"android": "active"}, + } + ] + }, + }, + platform="ios", + ) + + self.assertEqual(result.returncode, 0, result.stderr) + + def test_fails_closed_on_error_envelope(self) -> None: + result = self.run_preflight( + { + "status": "error", + "error": { + "code": "fetch_failed", + "message": "Insufficient permissions.", + }, + } + ) + + self.assertNotEqual(result.returncode, 0) + self.assertIn("fetch_failed", result.stderr) + self.assertIn("Insufficient permissions", result.stderr) + + def test_fails_closed_on_unexpected_shape(self) -> None: + result = self.run_preflight({"status": "success", "data": {}}) + + self.assertNotEqual(result.returncode, 0) + self.assertIn("missing releases array", result.stderr) + + def test_fails_closed_on_unreadable_platform_statuses(self) -> None: + result = self.run_preflight( + { + "status": "success", + "data": { + "releases": [ + { + "version": "1.0.20+841", + "platform_statuses": ["ios", "active"], + } + ] + }, + } + ) + + self.assertNotEqual(result.returncode, 0) + self.assertIn("unreadable platform_statuses", result.stderr) + + +if __name__ == "__main__": + unittest.main() diff --git a/.github/workflows/analytics.yaml b/.github/workflows/analytics.yaml index 916f629cd6..f34e4ee8e5 100644 --- a/.github/workflows/analytics.yaml +++ b/.github/workflows/analytics.yaml @@ -24,5 +24,5 @@ jobs: uses: VeryGoodOpenSource/very_good_workflows/.github/workflows/flutter_package.yml@v1 with: working_directory: "mobile/packages/analytics" - flutter_version: "3.44.0" + flutter_version: "3.44.9" min_coverage: 73 diff --git a/.github/workflows/app_update_repository.yaml b/.github/workflows/app_update_repository.yaml index 25d8dd324c..6b02d91392 100644 --- a/.github/workflows/app_update_repository.yaml +++ b/.github/workflows/app_update_repository.yaml @@ -24,5 +24,5 @@ jobs: uses: VeryGoodOpenSource/very_good_workflows/.github/workflows/flutter_package.yml@v1 with: working_directory: "mobile/packages/app_update_repository" - flutter_version: "3.44.0" + flutter_version: "3.44.9" min_coverage: 89 diff --git a/.github/workflows/app_version_client.yaml b/.github/workflows/app_version_client.yaml index eb4926d750..3c0b299862 100644 --- a/.github/workflows/app_version_client.yaml +++ b/.github/workflows/app_version_client.yaml @@ -24,5 +24,5 @@ jobs: uses: VeryGoodOpenSource/very_good_workflows/.github/workflows/flutter_package.yml@v1 with: working_directory: "mobile/packages/app_version_client" - flutter_version: "3.44.0" + flutter_version: "3.44.9" min_coverage: 76 diff --git a/.github/workflows/background_uploader.yaml b/.github/workflows/background_uploader.yaml index 9ebd10edcd..9556148038 100644 --- a/.github/workflows/background_uploader.yaml +++ b/.github/workflows/background_uploader.yaml @@ -24,5 +24,5 @@ jobs: uses: VeryGoodOpenSource/very_good_workflows/.github/workflows/flutter_package.yml@v1 with: working_directory: "mobile/packages/background_uploader" - flutter_version: "3.44.0" + flutter_version: "3.44.9" min_coverage: 69 diff --git a/.github/workflows/badge_repository.yaml b/.github/workflows/badge_repository.yaml index c1ee96f141..69cd7fe3e4 100644 --- a/.github/workflows/badge_repository.yaml +++ b/.github/workflows/badge_repository.yaml @@ -24,4 +24,4 @@ jobs: uses: VeryGoodOpenSource/very_good_workflows/.github/workflows/flutter_package.yml@v1 with: working_directory: "mobile/packages/badge_repository" - flutter_version: "3.44.0" + flutter_version: "3.44.9" diff --git a/.github/workflows/blossom_upload_service.yaml b/.github/workflows/blossom_upload_service.yaml index 7c388fd609..bf81559b36 100644 --- a/.github/workflows/blossom_upload_service.yaml +++ b/.github/workflows/blossom_upload_service.yaml @@ -24,5 +24,5 @@ jobs: uses: VeryGoodOpenSource/very_good_workflows/.github/workflows/flutter_package.yml@v1 with: working_directory: "mobile/packages/blossom_upload_service" - flutter_version: "3.44.0" + flutter_version: "3.44.9" min_coverage: 100 diff --git a/.github/workflows/blurhash_service.yaml b/.github/workflows/blurhash_service.yaml index dda7fad9a9..99ba96f110 100644 --- a/.github/workflows/blurhash_service.yaml +++ b/.github/workflows/blurhash_service.yaml @@ -24,4 +24,4 @@ jobs: uses: VeryGoodOpenSource/very_good_workflows/.github/workflows/flutter_package.yml@v1 with: working_directory: "mobile/packages/blurhash_service" - flutter_version: "3.44.0" + flutter_version: "3.44.9" diff --git a/.github/workflows/cache_sync.yml b/.github/workflows/cache_sync.yml index 732089c7b5..4bea10156c 100644 --- a/.github/workflows/cache_sync.yml +++ b/.github/workflows/cache_sync.yml @@ -24,5 +24,5 @@ jobs: uses: VeryGoodOpenSource/very_good_workflows/.github/workflows/flutter_package.yml@v1 with: working_directory: "mobile/packages/cache_sync" - flutter_version: "3.44.0" + flutter_version: "3.44.9" coverage_excludes: "**/*.g.dart" diff --git a/.github/workflows/caption_generator.yaml b/.github/workflows/caption_generator.yaml index 51b70e564e..539084d2a9 100644 --- a/.github/workflows/caption_generator.yaml +++ b/.github/workflows/caption_generator.yaml @@ -24,7 +24,7 @@ jobs: uses: VeryGoodOpenSource/very_good_workflows/.github/workflows/flutter_package.yml@v1 with: working_directory: "mobile/packages/caption_generator" - flutter_version: "3.44.0" + flutter_version: "3.44.9" # The Dart workflow above never compiles the native Kotlin, so a compile # error would pass green until the package is wired into an app build. @@ -45,7 +45,7 @@ jobs: - name: Setup Flutter uses: subosito/flutter-action@v2 with: - flutter-version: "3.44.0" + flutter-version: "3.44.9" channel: "stable" cache: true - name: Setup Gradle diff --git a/.github/workflows/categories_repository.yaml b/.github/workflows/categories_repository.yaml index 8fad2d7f3e..362c6b41e8 100644 --- a/.github/workflows/categories_repository.yaml +++ b/.github/workflows/categories_repository.yaml @@ -24,4 +24,4 @@ jobs: uses: VeryGoodOpenSource/very_good_workflows/.github/workflows/flutter_package.yml@v1 with: working_directory: "mobile/packages/categories_repository" - flutter_version: "3.44.0" + flutter_version: "3.44.9" diff --git a/.github/workflows/collaborator_repository.yaml b/.github/workflows/collaborator_repository.yaml index 45ae6e5a16..56561ee080 100644 --- a/.github/workflows/collaborator_repository.yaml +++ b/.github/workflows/collaborator_repository.yaml @@ -24,5 +24,5 @@ jobs: uses: VeryGoodOpenSource/very_good_workflows/.github/workflows/flutter_package.yml@v1 with: working_directory: "mobile/packages/collaborator_repository" - flutter_version: "3.44.0" + flutter_version: "3.44.9" min_coverage: 96 diff --git a/.github/workflows/comments_repository.yaml b/.github/workflows/comments_repository.yaml index 7009a1f9f9..15ab5cc770 100644 --- a/.github/workflows/comments_repository.yaml +++ b/.github/workflows/comments_repository.yaml @@ -24,5 +24,5 @@ jobs: uses: VeryGoodOpenSource/very_good_workflows/.github/workflows/flutter_package.yml@v1 with: working_directory: "mobile/packages/comments_repository" - flutter_version: "3.44.0" + flutter_version: "3.44.9" min_coverage: 80 diff --git a/.github/workflows/content_blocklist_repository.yaml b/.github/workflows/content_blocklist_repository.yaml index c8ef367e4b..89137ca7c1 100644 --- a/.github/workflows/content_blocklist_repository.yaml +++ b/.github/workflows/content_blocklist_repository.yaml @@ -24,4 +24,4 @@ jobs: uses: VeryGoodOpenSource/very_good_workflows/.github/workflows/flutter_package.yml@v1 with: working_directory: "mobile/packages/content_blocklist_repository" - flutter_version: "3.44.0" + flutter_version: "3.44.9" diff --git a/.github/workflows/content_policy.yaml b/.github/workflows/content_policy.yaml index 8d74c7e070..44a1fe5f88 100644 --- a/.github/workflows/content_policy.yaml +++ b/.github/workflows/content_policy.yaml @@ -24,4 +24,4 @@ jobs: uses: VeryGoodOpenSource/very_good_workflows/.github/workflows/flutter_package.yml@v1 with: working_directory: "mobile/packages/content_policy" - flutter_version: "3.44.0" + flutter_version: "3.44.9" diff --git a/.github/workflows/count_formatter.yaml b/.github/workflows/count_formatter.yaml index 58a8a2dfbe..027fb0e834 100644 --- a/.github/workflows/count_formatter.yaml +++ b/.github/workflows/count_formatter.yaml @@ -24,4 +24,4 @@ jobs: uses: VeryGoodOpenSource/very_good_workflows/.github/workflows/flutter_package.yml@v1 with: working_directory: "mobile/packages/count_formatter" - flutter_version: "3.44.0" + flutter_version: "3.44.9" diff --git a/.github/workflows/creator_sync.yaml b/.github/workflows/creator_sync.yaml index 90bbf2f927..ddce0251a3 100644 --- a/.github/workflows/creator_sync.yaml +++ b/.github/workflows/creator_sync.yaml @@ -24,4 +24,4 @@ jobs: uses: VeryGoodOpenSource/very_good_workflows/.github/workflows/flutter_package.yml@v1 with: working_directory: "mobile/packages/creator_sync" - flutter_version: "3.44.0" + flutter_version: "3.44.9" diff --git a/.github/workflows/curated_list_repository.yaml b/.github/workflows/curated_list_repository.yaml index 943217b6e4..7148ba8997 100644 --- a/.github/workflows/curated_list_repository.yaml +++ b/.github/workflows/curated_list_repository.yaml @@ -24,4 +24,4 @@ jobs: uses: VeryGoodOpenSource/very_good_workflows/.github/workflows/flutter_package.yml@v1 with: working_directory: "mobile/packages/curated_list_repository" - flutter_version: "3.44.0" + flutter_version: "3.44.9" diff --git a/.github/workflows/curation_repository.yaml b/.github/workflows/curation_repository.yaml index d3f1203891..c7d5c3e1e1 100644 --- a/.github/workflows/curation_repository.yaml +++ b/.github/workflows/curation_repository.yaml @@ -24,4 +24,4 @@ jobs: uses: VeryGoodOpenSource/very_good_workflows/.github/workflows/flutter_package.yml@v1 with: working_directory: "mobile/packages/curation_repository" - flutter_version: "3.44.0" + flutter_version: "3.44.9" diff --git a/.github/workflows/db_client.yaml b/.github/workflows/db_client.yaml index cdc7cc84d0..61a057f546 100644 --- a/.github/workflows/db_client.yaml +++ b/.github/workflows/db_client.yaml @@ -24,5 +24,5 @@ jobs: uses: VeryGoodOpenSource/very_good_workflows/.github/workflows/flutter_package.yml@v1 with: working_directory: "mobile/packages/db_client" - flutter_version: "3.44.0" + flutter_version: "3.44.9" min_coverage: 40 diff --git a/.github/workflows/divine_blurhash.yaml b/.github/workflows/divine_blurhash.yaml index 95d6d16acd..6ded7bf915 100644 --- a/.github/workflows/divine_blurhash.yaml +++ b/.github/workflows/divine_blurhash.yaml @@ -24,4 +24,4 @@ jobs: uses: VeryGoodOpenSource/very_good_workflows/.github/workflows/flutter_package.yml@v1 with: working_directory: "mobile/packages/divine_blurhash" - flutter_version: "3.44.0" + flutter_version: "3.44.9" diff --git a/.github/workflows/divine_camera.yaml b/.github/workflows/divine_camera.yaml index 3e14f8e9cb..fa1bb4f7d1 100644 --- a/.github/workflows/divine_camera.yaml +++ b/.github/workflows/divine_camera.yaml @@ -24,7 +24,7 @@ jobs: uses: VeryGoodOpenSource/very_good_workflows/.github/workflows/flutter_package.yml@v1 with: working_directory: "mobile/packages/divine_camera" - flutter_version: "3.44.0" + flutter_version: "3.44.9" android-unit-tests: name: Android Unit Tests @@ -42,7 +42,7 @@ jobs: - name: Setup Flutter uses: subosito/flutter-action@v2 with: - flutter-version: "3.44.0" + flutter-version: "3.44.9" channel: "stable" cache: true - name: Setup Gradle diff --git a/.github/workflows/divine_quick_actions.yaml b/.github/workflows/divine_quick_actions.yaml index cdd38e092f..ba5f66861e 100644 --- a/.github/workflows/divine_quick_actions.yaml +++ b/.github/workflows/divine_quick_actions.yaml @@ -24,5 +24,5 @@ jobs: uses: VeryGoodOpenSource/very_good_workflows/.github/workflows/flutter_package.yml@v1 with: working_directory: "mobile/packages/divine_quick_actions" - flutter_version: "3.44.0" + flutter_version: "3.44.9" min_coverage: 76 diff --git a/.github/workflows/divine_status_client.yaml b/.github/workflows/divine_status_client.yaml index 172664ff12..954a55803c 100644 --- a/.github/workflows/divine_status_client.yaml +++ b/.github/workflows/divine_status_client.yaml @@ -24,4 +24,4 @@ jobs: uses: VeryGoodOpenSource/very_good_workflows/.github/workflows/flutter_package.yml@v1 with: working_directory: "mobile/packages/divine_status_client" - flutter_version: "3.44.0" + flutter_version: "3.44.9" diff --git a/.github/workflows/divine_ui.yml b/.github/workflows/divine_ui.yml index b353f2a0af..affecef9c1 100644 --- a/.github/workflows/divine_ui.yml +++ b/.github/workflows/divine_ui.yml @@ -24,4 +24,4 @@ jobs: uses: VeryGoodOpenSource/very_good_workflows/.github/workflows/flutter_package.yml@v1 with: working_directory: "mobile/packages/divine_ui" - flutter_version: "3.44.0" + flutter_version: "3.44.9" diff --git a/.github/workflows/divine_video_player.yaml b/.github/workflows/divine_video_player.yaml index 145274169b..2a021db2e2 100644 --- a/.github/workflows/divine_video_player.yaml +++ b/.github/workflows/divine_video_player.yaml @@ -24,7 +24,7 @@ jobs: uses: VeryGoodOpenSource/very_good_workflows/.github/workflows/flutter_package.yml@v1 with: working_directory: "mobile/packages/divine_video_player" - flutter_version: "3.44.0" + flutter_version: "3.44.9" android-unit-tests: name: Android Unit Tests @@ -42,7 +42,7 @@ jobs: - name: Setup Flutter uses: subosito/flutter-action@v2 with: - flutter-version: "3.44.0" + flutter-version: "3.44.9" channel: "stable" cache: true - name: Setup Gradle diff --git a/.github/workflows/dm_repository.yaml b/.github/workflows/dm_repository.yaml index 71b30b5f1a..406e5a5c16 100644 --- a/.github/workflows/dm_repository.yaml +++ b/.github/workflows/dm_repository.yaml @@ -24,5 +24,5 @@ jobs: uses: VeryGoodOpenSource/very_good_workflows/.github/workflows/flutter_package.yml@v1 with: working_directory: "mobile/packages/dm_repository" - flutter_version: "3.44.0" + flutter_version: "3.44.9" min_coverage: 93 diff --git a/.github/workflows/feed_repository.yaml b/.github/workflows/feed_repository.yaml index a009cee6f5..bc6eaf97c1 100644 --- a/.github/workflows/feed_repository.yaml +++ b/.github/workflows/feed_repository.yaml @@ -24,5 +24,5 @@ jobs: uses: VeryGoodOpenSource/very_good_workflows/.github/workflows/flutter_package.yml@v1 with: working_directory: "mobile/packages/feed_repository" - flutter_version: "3.44.0" + flutter_version: "3.44.9" min_coverage: 64 diff --git a/.github/workflows/feed_tuning_repository.yaml b/.github/workflows/feed_tuning_repository.yaml index 1f6a8e10ee..b1b839c22b 100644 --- a/.github/workflows/feed_tuning_repository.yaml +++ b/.github/workflows/feed_tuning_repository.yaml @@ -24,4 +24,4 @@ jobs: uses: VeryGoodOpenSource/very_good_workflows/.github/workflows/flutter_package.yml@v1 with: working_directory: "mobile/packages/feed_tuning_repository" - flutter_version: "3.44.0" + flutter_version: "3.44.9" diff --git a/.github/workflows/follow_repository.yaml b/.github/workflows/follow_repository.yaml index 47fe3cd737..461cdfb999 100644 --- a/.github/workflows/follow_repository.yaml +++ b/.github/workflows/follow_repository.yaml @@ -24,4 +24,4 @@ jobs: uses: VeryGoodOpenSource/very_good_workflows/.github/workflows/flutter_package.yml@v1 with: working_directory: "mobile/packages/follow_repository" - flutter_version: "3.44.0" + flutter_version: "3.44.9" diff --git a/.github/workflows/funnelcake_api_client.yaml b/.github/workflows/funnelcake_api_client.yaml index 7d6a5a2164..45bb45ed10 100644 --- a/.github/workflows/funnelcake_api_client.yaml +++ b/.github/workflows/funnelcake_api_client.yaml @@ -24,5 +24,5 @@ jobs: uses: VeryGoodOpenSource/very_good_workflows/.github/workflows/flutter_package.yml@v1 with: working_directory: "mobile/packages/funnelcake_api_client" - flutter_version: "3.44.0" + flutter_version: "3.44.9" min_coverage: 44 diff --git a/.github/workflows/hashtag_repository.yaml b/.github/workflows/hashtag_repository.yaml index 5114651d88..860c91ea36 100644 --- a/.github/workflows/hashtag_repository.yaml +++ b/.github/workflows/hashtag_repository.yaml @@ -24,5 +24,5 @@ jobs: uses: VeryGoodOpenSource/very_good_workflows/.github/workflows/flutter_package.yml@v1 with: working_directory: "mobile/packages/hashtag_repository" - flutter_version: "3.44.0" + flutter_version: "3.44.9" min_coverage: 30 diff --git a/.github/workflows/iap_repository.yaml b/.github/workflows/iap_repository.yaml index 3602c11be3..56b6d5f6f9 100644 --- a/.github/workflows/iap_repository.yaml +++ b/.github/workflows/iap_repository.yaml @@ -24,5 +24,5 @@ jobs: uses: VeryGoodOpenSource/very_good_workflows/.github/workflows/flutter_package.yml@v1 with: working_directory: "mobile/packages/iap_repository" - flutter_version: "3.44.0" + flutter_version: "3.44.9" min_coverage: 90 diff --git a/.github/workflows/image_metadata_stripper.yaml b/.github/workflows/image_metadata_stripper.yaml index 991423c9e4..c1211a5310 100644 --- a/.github/workflows/image_metadata_stripper.yaml +++ b/.github/workflows/image_metadata_stripper.yaml @@ -24,4 +24,4 @@ jobs: uses: VeryGoodOpenSource/very_good_workflows/.github/workflows/flutter_package.yml@v1 with: working_directory: "mobile/packages/image_metadata_stripper" - flutter_version: "3.44.0" + flutter_version: "3.44.9" diff --git a/.github/workflows/infinite_video_feed.yaml b/.github/workflows/infinite_video_feed.yaml index ece6d379a3..98f805a5b0 100644 --- a/.github/workflows/infinite_video_feed.yaml +++ b/.github/workflows/infinite_video_feed.yaml @@ -24,4 +24,4 @@ jobs: uses: VeryGoodOpenSource/very_good_workflows/.github/workflows/flutter_package.yml@v1 with: working_directory: "mobile/packages/infinite_video_feed" - flutter_version: "3.44.0" + flutter_version: "3.44.9" diff --git a/.github/workflows/invite_api_client.yaml b/.github/workflows/invite_api_client.yaml index f528d6f5a5..6288607d24 100644 --- a/.github/workflows/invite_api_client.yaml +++ b/.github/workflows/invite_api_client.yaml @@ -24,5 +24,5 @@ jobs: uses: VeryGoodOpenSource/very_good_workflows/.github/workflows/flutter_package.yml@v1 with: working_directory: "mobile/packages/invite_api_client" - flutter_version: "3.44.0" + flutter_version: "3.44.9" min_coverage: 89 diff --git a/.github/workflows/keycast_flutter.yaml b/.github/workflows/keycast_flutter.yaml index 6b27d6e776..c2c0808e41 100644 --- a/.github/workflows/keycast_flutter.yaml +++ b/.github/workflows/keycast_flutter.yaml @@ -24,5 +24,5 @@ jobs: uses: VeryGoodOpenSource/very_good_workflows/.github/workflows/flutter_package.yml@v1 with: working_directory: "mobile/packages/keycast_flutter" - flutter_version: "3.44.0" + flutter_version: "3.44.9" min_coverage: 44 diff --git a/.github/workflows/likes_repository.yaml b/.github/workflows/likes_repository.yaml index f576098701..94254037a9 100644 --- a/.github/workflows/likes_repository.yaml +++ b/.github/workflows/likes_repository.yaml @@ -24,5 +24,5 @@ jobs: uses: VeryGoodOpenSource/very_good_workflows/.github/workflows/flutter_package.yml@v1 with: working_directory: "mobile/packages/likes_repository" - flutter_version: "3.44.0" + flutter_version: "3.44.9" min_coverage: 62 diff --git a/.github/workflows/media_cache.yml b/.github/workflows/media_cache.yml index e27934d971..f0e107b91c 100644 --- a/.github/workflows/media_cache.yml +++ b/.github/workflows/media_cache.yml @@ -24,5 +24,5 @@ jobs: uses: VeryGoodOpenSource/very_good_workflows/.github/workflows/flutter_package.yml@v1 with: working_directory: "mobile/packages/media_cache" - flutter_version: "3.44.0" + flutter_version: "3.44.9" min_coverage: 98 diff --git a/.github/workflows/mobile_ci.yaml b/.github/workflows/mobile_ci.yaml index 19316e9f33..96e5815f08 100644 --- a/.github/workflows/mobile_ci.yaml +++ b/.github/workflows/mobile_ci.yaml @@ -83,6 +83,13 @@ jobs: # undeclared group fails every workflow in the file and no in-script # guard can catch it (#7203). run: bash mobile/scripts/check_codemagic_groups.sh + ci-config-tests: + name: CI Configuration Tests + runs-on: ubuntu-24.04 + steps: + - uses: actions/checkout@v7 + - name: Run CI configuration unit tests + run: python3 -m unittest discover -s .github/scripts/tests generated-files: name: Generated Files @@ -102,7 +109,7 @@ jobs: - name: 🐦 Setup Flutter uses: subosito/flutter-action@v2 with: - flutter-version: "3.44.0" + flutter-version: "3.44.9" channel: "stable" cache: true - name: 📦 Install dependencies @@ -315,7 +322,7 @@ jobs: - name: 🐦 Setup Flutter uses: subosito/flutter-action@v2 with: - flutter-version: "3.44.0" + flutter-version: "3.44.9" channel: "stable" cache: true - name: 📦 Install dependencies @@ -339,7 +346,7 @@ jobs: - name: 🐦 Setup Flutter uses: subosito/flutter-action@v2 with: - flutter-version: "3.44.0" + flutter-version: "3.44.9" channel: "stable" cache: true - name: 📦 Install dependencies @@ -365,7 +372,7 @@ jobs: - name: 🐦 Setup Flutter uses: subosito/flutter-action@v2 with: - flutter-version: "3.44.0" + flutter-version: "3.44.9" channel: "stable" cache: true - name: 📦 Install dependencies @@ -491,7 +498,7 @@ jobs: - name: 🐦 Setup Flutter uses: subosito/flutter-action@v2 with: - flutter-version: "3.44.0" + flutter-version: "3.44.9" channel: "stable" cache: true - name: 📦 Install dependencies @@ -542,6 +549,7 @@ jobs: needs: - changes - codemagic-groups + - ci-config-tests - generated-files - format - analyze @@ -554,6 +562,7 @@ jobs: env: CODEMAGIC_GROUPS_RESULT: ${{ needs.codemagic-groups.result }} GENERATED_FILES_RESULT: ${{ needs.generated-files.result }} + CI_CONFIG_TESTS_RESULT: ${{ needs.ci-config-tests.result }} FORMAT_RESULT: ${{ needs.format.result }} ANALYZE_RESULT: ${{ needs.analyze.result }} TESTS_RESULT: ${{ needs.tests.result }} @@ -565,6 +574,7 @@ jobs: echo "app-ci-required: ${APP_CI_REQUIRED}" echo "codemagic-groups: ${CODEMAGIC_GROUPS_RESULT}" echo "generated-files: ${GENERATED_FILES_RESULT}" + echo "ci-config-tests: ${CI_CONFIG_TESTS_RESULT}" echo "format: ${FORMAT_RESULT}" echo "analyze: ${ANALYZE_RESULT}" echo "tests: ${TESTS_RESULT}" @@ -593,6 +603,7 @@ jobs: fi results=( + "${CI_CONFIG_TESTS_RESULT}" "${GENERATED_FILES_RESULT}" "${FORMAT_RESULT}" "${ANALYZE_RESULT}" diff --git a/.github/workflows/mobile_pr_preview_build.yml b/.github/workflows/mobile_pr_preview_build.yml index 4967f6acab..be977be8a4 100644 --- a/.github/workflows/mobile_pr_preview_build.yml +++ b/.github/workflows/mobile_pr_preview_build.yml @@ -37,7 +37,7 @@ jobs: - name: Setup Flutter uses: subosito/flutter-action@v2 with: - flutter-version: "3.44.0" + flutter-version: "3.44.9" channel: stable cache: true diff --git a/.github/workflows/mobile_service_integration_tests.yaml b/.github/workflows/mobile_service_integration_tests.yaml index d43f0041d1..fb9ab167e0 100644 --- a/.github/workflows/mobile_service_integration_tests.yaml +++ b/.github/workflows/mobile_service_integration_tests.yaml @@ -31,7 +31,7 @@ jobs: - name: 🐦 Setup Flutter uses: subosito/flutter-action@v2 with: - flutter-version: "3.44.0" + flutter-version: "3.44.9" channel: "stable" cache: true # `flutter test integration_test/...` runs in integration-test mode, which diff --git a/.github/workflows/mobile_web_production_deploy.yml b/.github/workflows/mobile_web_production_deploy.yml index b17fb5bf08..bba9cf6faa 100644 --- a/.github/workflows/mobile_web_production_deploy.yml +++ b/.github/workflows/mobile_web_production_deploy.yml @@ -31,7 +31,7 @@ jobs: - name: Setup Flutter uses: subosito/flutter-action@v2 with: - flutter-version: "3.44.0" + flutter-version: "3.44.9" channel: stable cache: true diff --git a/.github/workflows/models_ci.yaml b/.github/workflows/models_ci.yaml index fca92af4ea..edacd1eca8 100644 --- a/.github/workflows/models_ci.yaml +++ b/.github/workflows/models_ci.yaml @@ -24,5 +24,5 @@ jobs: uses: VeryGoodOpenSource/very_good_workflows/.github/workflows/flutter_package.yml@v1 with: working_directory: "mobile/packages/models" - flutter_version: "3.44.0" + flutter_version: "3.44.9" min_coverage: 25 diff --git a/.github/workflows/nostr_app_bridge_repository.yaml b/.github/workflows/nostr_app_bridge_repository.yaml index c1a8ad9ec2..257d55d08e 100644 --- a/.github/workflows/nostr_app_bridge_repository.yaml +++ b/.github/workflows/nostr_app_bridge_repository.yaml @@ -24,5 +24,5 @@ jobs: uses: VeryGoodOpenSource/very_good_workflows/.github/workflows/flutter_package.yml@v1 with: working_directory: "mobile/packages/nostr_app_bridge_repository" - flutter_version: "3.44.0" + flutter_version: "3.44.9" min_coverage: 78 diff --git a/.github/workflows/nostr_client.yaml b/.github/workflows/nostr_client.yaml index 585a0c3dcf..b71b2594e6 100644 --- a/.github/workflows/nostr_client.yaml +++ b/.github/workflows/nostr_client.yaml @@ -24,5 +24,5 @@ jobs: uses: VeryGoodOpenSource/very_good_workflows/.github/workflows/flutter_package.yml@v1 with: working_directory: "mobile/packages/nostr_client" - flutter_version: "3.44.0" + flutter_version: "3.44.9" min_coverage: 82 diff --git a/.github/workflows/nostr_key_manager.yml b/.github/workflows/nostr_key_manager.yml index fc3e522c50..d467e67e97 100644 --- a/.github/workflows/nostr_key_manager.yml +++ b/.github/workflows/nostr_key_manager.yml @@ -24,5 +24,5 @@ jobs: uses: VeryGoodOpenSource/very_good_workflows/.github/workflows/flutter_package.yml@v1 with: working_directory: "mobile/packages/nostr_key_manager" - flutter_version: "3.44.0" + flutter_version: "3.44.9" min_coverage: 52 diff --git a/.github/workflows/nostr_sdk.yaml b/.github/workflows/nostr_sdk.yaml index 69ef295652..190d3949f1 100644 --- a/.github/workflows/nostr_sdk.yaml +++ b/.github/workflows/nostr_sdk.yaml @@ -24,5 +24,5 @@ jobs: uses: VeryGoodOpenSource/very_good_workflows/.github/workflows/flutter_package.yml@v1 with: working_directory: "mobile/packages/nostr_sdk" - flutter_version: "3.44.0" + flutter_version: "3.44.9" min_coverage: 20 diff --git a/.github/workflows/notification_repository.yaml b/.github/workflows/notification_repository.yaml index 0726ceecfc..4097637228 100644 --- a/.github/workflows/notification_repository.yaml +++ b/.github/workflows/notification_repository.yaml @@ -24,5 +24,5 @@ jobs: uses: VeryGoodOpenSource/very_good_workflows/.github/workflows/flutter_package.yml@v1 with: working_directory: "mobile/packages/notification_repository" - flutter_version: "3.44.0" + flutter_version: "3.44.9" min_coverage: 94 diff --git a/.github/workflows/people_lists_repository.yaml b/.github/workflows/people_lists_repository.yaml index 7efe0bdfa7..685c9acb23 100644 --- a/.github/workflows/people_lists_repository.yaml +++ b/.github/workflows/people_lists_repository.yaml @@ -24,5 +24,5 @@ jobs: uses: VeryGoodOpenSource/very_good_workflows/.github/workflows/flutter_package.yml@v1 with: working_directory: "mobile/packages/people_lists_repository" - flutter_version: "3.44.0" + flutter_version: "3.44.9" min_coverage: 95 diff --git a/.github/workflows/permissions_service.yaml b/.github/workflows/permissions_service.yaml index 996a0bf585..1c354ec548 100644 --- a/.github/workflows/permissions_service.yaml +++ b/.github/workflows/permissions_service.yaml @@ -24,5 +24,5 @@ jobs: uses: VeryGoodOpenSource/very_good_workflows/.github/workflows/flutter_package.yml@v1 with: working_directory: "mobile/packages/permissions_service" - flutter_version: "3.44.0" + flutter_version: "3.44.9" min_coverage: 22 diff --git a/.github/workflows/profile_repository.yaml b/.github/workflows/profile_repository.yaml index 7bb5d746ac..93d4f29b6f 100644 --- a/.github/workflows/profile_repository.yaml +++ b/.github/workflows/profile_repository.yaml @@ -24,4 +24,4 @@ jobs: uses: VeryGoodOpenSource/very_good_workflows/.github/workflows/flutter_package.yml@v1 with: working_directory: "mobile/packages/profile_repository" - flutter_version: "3.44.0" + flutter_version: "3.44.9" diff --git a/.github/workflows/reposts_repository.yaml b/.github/workflows/reposts_repository.yaml index c9d31ecd70..4108185a27 100644 --- a/.github/workflows/reposts_repository.yaml +++ b/.github/workflows/reposts_repository.yaml @@ -24,4 +24,4 @@ jobs: uses: VeryGoodOpenSource/very_good_workflows/.github/workflows/flutter_package.yml@v1 with: working_directory: "mobile/packages/reposts_repository" - flutter_version: "3.44.0" + flutter_version: "3.44.9" diff --git a/.github/workflows/sound_service.yaml b/.github/workflows/sound_service.yaml index 08b510a27d..0e0e7826a9 100644 --- a/.github/workflows/sound_service.yaml +++ b/.github/workflows/sound_service.yaml @@ -24,4 +24,4 @@ jobs: uses: VeryGoodOpenSource/very_good_workflows/.github/workflows/flutter_package.yml@v1 with: working_directory: "mobile/packages/sound_service" - flutter_version: "3.44.0" + flutter_version: "3.44.9" diff --git a/.github/workflows/sounds_repository.yaml b/.github/workflows/sounds_repository.yaml index 1b1012a2d2..0e8bcb0f2a 100644 --- a/.github/workflows/sounds_repository.yaml +++ b/.github/workflows/sounds_repository.yaml @@ -24,4 +24,4 @@ jobs: uses: VeryGoodOpenSource/very_good_workflows/.github/workflows/flutter_package.yml@v1 with: working_directory: "mobile/packages/sounds_repository" - flutter_version: "3.44.0" + flutter_version: "3.44.9" diff --git a/.github/workflows/text_sanitizer.yaml b/.github/workflows/text_sanitizer.yaml index 27ff3e5154..82c3bcbf0a 100644 --- a/.github/workflows/text_sanitizer.yaml +++ b/.github/workflows/text_sanitizer.yaml @@ -24,4 +24,4 @@ jobs: uses: VeryGoodOpenSource/very_good_workflows/.github/workflows/flutter_package.yml@v1 with: working_directory: "mobile/packages/text_sanitizer" - flutter_version: "3.44.0" + flutter_version: "3.44.9" diff --git a/.github/workflows/time_formatter.yaml b/.github/workflows/time_formatter.yaml index 1da5d09877..d949260b37 100644 --- a/.github/workflows/time_formatter.yaml +++ b/.github/workflows/time_formatter.yaml @@ -24,5 +24,5 @@ jobs: uses: VeryGoodOpenSource/very_good_workflows/.github/workflows/flutter_package.yml@v1 with: working_directory: "mobile/packages/time_formatter" - flutter_version: "3.44.0" + flutter_version: "3.44.9" min_coverage: 100 diff --git a/.github/workflows/tv_static_effect.yaml b/.github/workflows/tv_static_effect.yaml index 6b3e5db85c..554c2a6230 100644 --- a/.github/workflows/tv_static_effect.yaml +++ b/.github/workflows/tv_static_effect.yaml @@ -24,4 +24,4 @@ jobs: uses: VeryGoodOpenSource/very_good_workflows/.github/workflows/flutter_package.yml@v1 with: working_directory: "mobile/packages/tv_static_effect" - flutter_version: "3.44.0" + flutter_version: "3.44.9" diff --git a/.github/workflows/unified_logger.yaml b/.github/workflows/unified_logger.yaml index 7f25bc8412..02232b6cd8 100644 --- a/.github/workflows/unified_logger.yaml +++ b/.github/workflows/unified_logger.yaml @@ -24,5 +24,5 @@ jobs: uses: VeryGoodOpenSource/very_good_workflows/.github/workflows/flutter_package.yml@v1 with: working_directory: "mobile/packages/unified_logger" - flutter_version: "3.44.0" + flutter_version: "3.44.9" min_coverage: 40 diff --git a/.github/workflows/verifier_client.yaml b/.github/workflows/verifier_client.yaml index 932fad31e5..57afafe267 100644 --- a/.github/workflows/verifier_client.yaml +++ b/.github/workflows/verifier_client.yaml @@ -24,5 +24,5 @@ jobs: uses: VeryGoodOpenSource/very_good_workflows/.github/workflows/flutter_package.yml@v1 with: working_directory: "mobile/packages/verifier_client" - flutter_version: "3.44.0" + flutter_version: "3.44.9" min_coverage: 76 diff --git a/.github/workflows/video_event_cache.yaml b/.github/workflows/video_event_cache.yaml index ff8f465524..e5489960fc 100644 --- a/.github/workflows/video_event_cache.yaml +++ b/.github/workflows/video_event_cache.yaml @@ -24,5 +24,5 @@ jobs: uses: VeryGoodOpenSource/very_good_workflows/.github/workflows/flutter_package.yml@v1 with: working_directory: "mobile/packages/video_event_cache" - flutter_version: "3.44.0" + flutter_version: "3.44.9" min_coverage: 0 diff --git a/.github/workflows/videos_repository.yaml b/.github/workflows/videos_repository.yaml index 4d300d473b..87b0d448db 100644 --- a/.github/workflows/videos_repository.yaml +++ b/.github/workflows/videos_repository.yaml @@ -24,4 +24,4 @@ jobs: uses: VeryGoodOpenSource/very_good_workflows/.github/workflows/flutter_package.yml@v1 with: working_directory: "mobile/packages/videos_repository" - flutter_version: "3.44.0" + flutter_version: "3.44.9" diff --git a/AGENTS.md b/AGENTS.md index 3d51b503cc..98691e5399 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -4,7 +4,7 @@ - Most implementation work is in `mobile/`. The main Flutter entry points are `mobile/lib/main.dart` and `mobile/lib/router/app_router.dart`. - Shared reusable logic belongs in the owning package under `mobile/packages/`, not as app-layer duplication. -- Start with current code and focused docs, especially `CONTRIBUTING.md`, `docs/STATE_MANAGEMENT.md`, `docs/BLOC_UI_MIGRATION_PRD.md`, `docs/NOSTR_EVENT_TYPES.md`, `mobile/docs/NOSTR_VIDEO_EVENTS.md`, `mobile/docs/DESIGN_SYSTEM_COMPONENTS.md`, and `mobile/docs/GOLDEN_TESTING_GUIDE.md`. +- Start with current code and focused docs, especially `CONTRIBUTING.md`, `docs/STATE_MANAGEMENT.md`, `docs/BLOC_UI_MIGRATION_PRD.md`, `docs/NOSTR_EVENT_TYPES.md`, `mobile/docs/NOSTR_VIDEO_EVENTS.md`, `mobile/docs/DESIGN_SYSTEM_COMPONENTS.md`, `mobile/docs/GOLDEN_TESTING_GUIDE.md`, and `mobile/docs/SHOREBIRD_CODE_PUSH.md`. - Older docs can drift. If documentation conflicts, trust the current implementation, targeted tests, and the newest focused doc over historical notes. ## Divine Context And Brain @@ -158,6 +158,22 @@ If a Divine Brain search or ask tool is available, you may use it for company me - User-installed CAs are not trusted in any build. If you need to proxy-debug with Charles or mitmproxy, add a single `` line to `` in `mobile/android/app/src/main/res/xml/network_security_config.xml` in your working copy and don't commit it; CI's transport-security guard will block any commit that re-enables user-CA trust. (For iOS and macOS, plist-edit `NSAppTransportSecurity` similarly and revert before commit.) - If you add a new exception to any native config, update `mobile/scripts/check_native_transport_security.sh` so the guard recognises the allowance. It covers four files: the Android XML plus the `Runner` plists for iOS and macOS and the iOS notification-service extension. +## Shorebird Code Push + +- The Play AAB and App Store IPA are built by `shorebird release`, not + `flutter build`. That command links the Shorebird updater into the engine — + a store artifact built with plain `flutter build` is **permanently + unpatchable**. Never swap those steps back in `codemagic.yaml`. +- Never add `--allow-native-diffs` or `--allow-asset-diffs`. Shorebird's + refusal to patch across native or asset changes is a correctness guard; a fix + needing native changes needs a store release. +- Patches are manual-only, gated behind `CONFIRM_PATCH`, and published to the + Shorebird `staging` track first. Validate the staged patch, then promote it + to `stable`. Rollback exists, but it is emergency recovery rather than a + substitute for staging and review. +- Full detail, including the patch runbook and known gotchas: + `mobile/docs/SHOREBIRD_CODE_PUSH.md`. + ## Zapstore Publishing Notes - Do not complicate Zapstore publish handoff. Let Rabble run `zsp` directly unless explicitly asked to wrap or automate it. diff --git a/codemagic.yaml b/codemagic.yaml index 5989382e58..f114d77c57 100644 --- a/codemagic.yaml +++ b/codemagic.yaml @@ -29,6 +29,16 @@ # 9 - Android publishing reads the service account from environment variable # group "google_play_credentials": # - GCLOUD_SERVICE_ACCOUNT_CREDENTIALS +# 10 - Create environment variable group "shorebird_credentials" with: +# - SHOREBIRD_TOKEN (mark as secure) — an API key from the Shorebird +# console under Account > API Keys. `shorebird login:ci` is removed; +# CI authenticates with API keys only. +# - SHOREBIRD_PATCH_PUBLIC_KEY (public PEM used by `shorebird release`) +# - SHOREBIRD_PATCH_PRIVATE_KEY (secure private PEM used by `shorebird patch`) +# Store each PEM as a single-line value whose newlines are written as +# literal `\n` sequences. The CI step validates the decoded PEM envelope. +# The key must belong to an account with access to the app_id in +# mobile/shorebird.yaml, which lives in the Divine organization. # # Every group named under a workflow's `groups:` key must appear in this # checklist. Codemagic validates this whole file before it provisions a machine, @@ -153,22 +163,177 @@ definitions: } >> "$GRADLE_HOME/gradle.properties" echo "Effective $GRADLE_HOME/gradle.properties:" cat "$GRADLE_HOME/gradle.properties" - - &build_apk - name: Build Android APK + - &shorebird_install + name: Install Shorebird script: | - flutter build apk --release --split-per-abi --build-number=$PROJECT_BUILD_NUMBER \ - --dart-define=ZENDESK_APP_ID=$ZENDESK_APP_ID \ - --dart-define=ZENDESK_CLIENT_ID=$ZENDESK_CLIENT_ID \ - --dart-define=ZENDESK_URL=$ZENDESK_URL \ - --dart-define=DEFAULT_ENV=${{ inputs.DEFAULT_ENV }} \ - --dart-define=PROOFMODE_SIGNING_SERVER_ENDPOINT=$PROOFMODE_SIGNING_SERVER_ENDPOINT \ - --dart-define=PROOFMODE_SIGNING_SERVER_TOKEN=$PROOFMODE_SIGNING_SERVER_TOKEN \ - --dart-define=SUPPORTERS_API_BASE_URL=$SUPPORTERS_API_BASE_URL \ - --dart-define=FF_DIVINE_SUPPORTERS=$FF_DIVINE_SUPPORTERS - - &build_aab - name: Build Android AAB + set -euo pipefail + SHOREBIRD_BIN="$HOME/.shorebird/bin/shorebird" + EXPECTED_SHOREBIRD_VERSION="Shorebird 1.6.117" + SHOREBIRD_VERSION_OUTPUT="" + if [ -x "$SHOREBIRD_BIN" ]; then + SHOREBIRD_VERSION_OUTPUT=$("$SHOREBIRD_BIN" --version 2>/dev/null || true) + fi + if ! printf '%s\n' "$SHOREBIRD_VERSION_OUTPUT" | grep -Fq "$EXPECTED_SHOREBIRD_VERSION"; then + curl --proto '=https' --tlsv1.2 https://raw.githubusercontent.com/shorebirdtech/install/main/install.sh -sSf | bash -s -- --force + SHOREBIRD_VERSION_OUTPUT=$("$SHOREBIRD_BIN" --version) + fi + echo PATH="$HOME/.shorebird/bin:$PATH" >> $CM_ENV + echo "$SHOREBIRD_VERSION_OUTPUT" + if ! printf '%s\n' "$SHOREBIRD_VERSION_OUTPUT" | grep -Fq "$EXPECTED_SHOREBIRD_VERSION"; then + echo "ERROR: expected $EXPECTED_SHOREBIRD_VERSION; update and validate the pinned version before continuing." + exit 1 + fi + - &write_shorebird_dart_defines + name: Write Shorebird dart-defines script: | - # Use the latest Play versionCode as the primary source of truth. + mkdir -p build/shorebird + DEFAULT_ENV="${{ inputs.DEFAULT_ENV }}" ruby <<'RUBY' + require 'json' + + required_names = %w[ + ZENDESK_APP_ID + ZENDESK_CLIENT_ID + ZENDESK_URL + PROOFMODE_SIGNING_SERVER_ENDPOINT + PROOFMODE_SIGNING_SERVER_TOKEN + ] + + optional_names = %w[ + SUPPORTERS_API_BASE_URL + FF_DIVINE_SUPPORTERS + ] + + missing = required_names.reject { |name| ENV.key?(name) && !ENV.fetch(name).empty? } + unless missing.empty? + abort("missing Shorebird dart-define environment variables: #{missing.join(', ')}") + end + + defines = required_names.to_h { |name| [name, ENV.fetch(name)] } + optional_names.each do |name| + defines[name] = ENV.fetch(name, '') + end + defines['DEFAULT_ENV'] = ENV.fetch('DEFAULT_ENV') + + File.write( + 'build/shorebird/dart_defines.json', + JSON.pretty_generate(defines) + "\n", + ) + File.chmod(0o600, 'build/shorebird/dart_defines.json') + RUBY + - &write_shorebird_public_key + name: Write Shorebird patch public key + script: | + mkdir -p build/shorebird + ruby <<'RUBY' + keys = { + 'SHOREBIRD_PATCH_PUBLIC_KEY' => 'build/shorebird/patch_public_key.pem', + } + + missing = keys.keys.reject { |name| ENV.key?(name) && !ENV.fetch(name).empty? } + unless missing.empty? + abort("missing Shorebird patch signing variables: #{missing.join(', ')}") + end + + keys.each do |name, path| + pem = ENV.fetch(name).gsub('\\n', "\n") + unless pem.match?(/\A-----BEGIN PUBLIC KEY-----\n.+\n-----END PUBLIC KEY-----\n?\z/m) + abort("invalid PEM encoding for #{name}; use literal \\n sequences") + end + File.write(path, pem) + File.chmod(0o644, path) + end + RUBY + - &write_shorebird_private_key + name: Write Shorebird patch private key + script: | + mkdir -p build/shorebird + ruby <<'RUBY' + name = 'SHOREBIRD_PATCH_PRIVATE_KEY' + path = 'build/shorebird/patch_private_key.pem' + unless ENV.key?(name) && !ENV.fetch(name).empty? + abort("missing Shorebird patch signing variable: #{name}") + end + pem = ENV.fetch(name).gsub('\\n', "\n") + unless pem.match?(/\A-----BEGIN (?:EC |RSA )?PRIVATE KEY-----\n.+\n-----END (?:EC |RSA )?PRIVATE KEY-----\n?\z/m) + abort("invalid PEM encoding for #{name}; use literal \\n sequences") + end + File.write(path, pem) + File.chmod(0o600, path) + RUBY + - &prepare_patch_source + name: Verify patch source + script: | + if [ "$CM_BRANCH" != "main" ]; then + echo "Refusing to patch from branch '$CM_BRANCH'." + echo "Patch workflows must run from main after the fix has merged." + exit 1 + fi + if [ -z "${{ inputs.RELEASE_COMMIT }}" ]; then + echo "Refusing to patch: RELEASE_COMMIT is required." + echo "Use the git commit that produced the target store release." + exit 1 + fi + if ! printf '%s\n' "${{ inputs.RELEASE_COMMIT }}" | grep -Eq '^[0-9a-fA-F]{40}$'; then + echo "Refusing to patch: RELEASE_COMMIT must be a full 40-character commit SHA." + exit 1 + fi + if [ "$(git rev-parse --is-shallow-repository)" = "true" ]; then + git fetch --quiet --unshallow origin main + else + git fetch --quiet origin main + fi + if ! git cat-file -e "${{ inputs.RELEASE_COMMIT }}^{commit}" 2>/dev/null; then + echo "Refusing to patch: RELEASE_COMMIT '${{ inputs.RELEASE_COMMIT }}' is not a commit in this checkout." + exit 1 + fi + if ! git merge-base --is-ancestor "${{ inputs.RELEASE_COMMIT }}" HEAD; then + echo "Refusing to patch: RELEASE_COMMIT must be an ancestor of HEAD." + echo "release commit: ${{ inputs.RELEASE_COMMIT }}" + echo "current head: $(git rev-parse HEAD)" + exit 1 + fi + BLOCKED_PATCH_PATHS=$(git diff --name-only "${{ inputs.RELEASE_COMMIT }}..HEAD" -- \ + 'android/**' \ + 'ios/**' \ + 'macos/**' \ + 'web/**' \ + 'assets/**' \ + 'shaders/**' \ + 'pubspec.yaml' \ + 'pubspec.lock' \ + 'shorebird.yaml' \ + 'packages/**/android/**' \ + 'packages/**/assets/**' \ + 'packages/**/shaders/**' \ + 'packages/**/darwin/**' \ + 'packages/**/ios/**' \ + 'packages/**/macos/**' \ + 'packages/**/pubspec.yaml') + if [ -n "$BLOCKED_PATCH_PATHS" ]; then + echo "Refusing to patch: this diff contains native, asset, dependency, or Shorebird config changes." + echo + echo "$BLOCKED_PATCH_PATHS" + echo + echo "Cut a normal store release instead of a Shorebird patch." + exit 1 + fi + echo "Patch source diff from release commit to current HEAD:" + git log --oneline --no-merges "${{ inputs.RELEASE_COMMIT }}..HEAD" + if git diff --quiet "${{ inputs.RELEASE_COMMIT }}..HEAD" -- '*.dart'; then + echo "No Dart changes found between release commit and HEAD." + else + echo + echo "Dart files changed since the release commit:" + git diff --name-only "${{ inputs.RELEASE_COMMIT }}..HEAD" -- '*.dart' + fi + - &preflight_shorebird_android_release + name: Preflight Android Shorebird release + script: | + BUILD_NAME=$(ruby -ne 'if $_ =~ /^version:\s*([0-9]+\.[0-9]+\.[0-9]+)\+/; puts $1; exit; end' pubspec.yaml) + if [ -z "$BUILD_NAME" ]; then + echo "ERROR: could not read build name from pubspec.yaml version." + exit 1 + fi LATEST_GOOGLE_PLAY_BUILD_NUMBER=$(google-play get-latest-build-number --package-name "co.openvine.app" 2>/dev/null || true) BUILD_NUMBER=$PROJECT_BUILD_NUMBER case "$LATEST_GOOGLE_PLAY_BUILD_NUMBER" in @@ -181,15 +346,142 @@ definitions: fi ;; esac - flutter build appbundle --release --build-number=$BUILD_NUMBER \ - --dart-define=ZENDESK_APP_ID=$ZENDESK_APP_ID \ - --dart-define=ZENDESK_CLIENT_ID=$ZENDESK_CLIENT_ID \ - --dart-define=ZENDESK_URL=$ZENDESK_URL \ - --dart-define=DEFAULT_ENV=${{ inputs.DEFAULT_ENV }} \ - --dart-define=PROOFMODE_SIGNING_SERVER_ENDPOINT=$PROOFMODE_SIGNING_SERVER_ENDPOINT \ - --dart-define=PROOFMODE_SIGNING_SERVER_TOKEN=$PROOFMODE_SIGNING_SERVER_TOKEN \ - --dart-define=SUPPORTERS_API_BASE_URL=$SUPPORTERS_API_BASE_URL \ - --dart-define=FF_DIVINE_SUPPORTERS=$FF_DIVINE_SUPPORTERS + RELEASE_VERSION="$BUILD_NAME+$BUILD_NUMBER" + { + echo "SHOREBIRD_BUILD_NAME=$BUILD_NAME" + echo "SHOREBIRD_BUILD_NUMBER=$BUILD_NUMBER" + echo "SHOREBIRD_RELEASE_VERSION=$RELEASE_VERSION" + } > build/shorebird/android_release.env + echo "Android Shorebird release version: $RELEASE_VERSION" + if ! shorebird releases list --platform=android --json > build/shorebird/release_info.json 2> build/shorebird/release_info.err; then + echo "ERROR: could not list existing Shorebird releases." + if [ -s build/shorebird/release_info.err ]; then + sed 's/^/stderr: /' build/shorebird/release_info.err + fi + if [ -s build/shorebird/release_info.json ]; then + sed 's/^/stdout: /' build/shorebird/release_info.json + fi + exit 1 + fi + ruby scripts/shorebird_release_preflight.rb build/shorebird/release_info.json android "$RELEASE_VERSION" + - &preflight_shorebird_ios_release + name: Preflight iOS Shorebird release + script: | + set -euo pipefail + BUILD_NAME=$(ruby -ne 'if $_ =~ /^version:\s*([0-9]+\.[0-9]+\.[0-9]+)\+/; puts $1; exit; end' pubspec.yaml) + if [ -z "$BUILD_NAME" ]; then + echo "ERROR: could not read build name from pubspec.yaml version." + exit 1 + fi + APP_STORE_APP_ID=6747959501 # co.openvine.app + ASC_LOOKUP_LOG=$(mktemp) + ASC_STORE_VERSION_JSON=$(mktemp) + trap 'rm -f "$ASC_LOOKUP_LOG" "$ASC_STORE_VERSION_JSON"' EXIT + if ! app-store-connect get-latest-app-store-build-number "$APP_STORE_APP_ID" \ + --platform=IOS --include-version --json > "$ASC_STORE_VERSION_JSON" 2>"$ASC_LOOKUP_LOG"; then + echo "ERROR: could not read the latest approved App Store version." + if [ -s "$ASC_LOOKUP_LOG" ]; then + echo " App Store Connect lookup output:" + sed 's/^/ /' "$ASC_LOOKUP_LOG" + fi + echo " Refusing to create a Shorebird release that Apple may reject after the build." + exit 1 + fi + ruby scripts/ios_store_version_preflight.rb "$ASC_STORE_VERSION_JSON" "$BUILD_NAME" + : > "$ASC_LOOKUP_LOG" + if ! LATEST_IOS_BUILD_NUMBER=$(app-store-connect get-latest-build-number "$APP_STORE_APP_ID" 2>"$ASC_LOOKUP_LOG"); then + LATEST_IOS_BUILD_NUMBER= + fi + case "$LATEST_IOS_BUILD_NUMBER" in + ''|*[!0-9]*) + echo "ERROR: could not read the latest App Store Connect build number." + if [ -s "$ASC_LOOKUP_LOG" ]; then + echo " App Store Connect lookup output:" + sed 's/^/ /' "$ASC_LOOKUP_LOG" + fi + echo " Refusing to guess a build number Apple may reject after upload." + exit 1 + ;; + *) + BUILD_NUMBER=$((LATEST_IOS_BUILD_NUMBER + 1)) + ;; + esac + RELEASE_VERSION="$BUILD_NAME+$BUILD_NUMBER" + { + echo "SHOREBIRD_BUILD_NAME=$BUILD_NAME" + echo "SHOREBIRD_BUILD_NUMBER=$BUILD_NUMBER" + echo "SHOREBIRD_RELEASE_VERSION=$RELEASE_VERSION" + } > build/shorebird/ios_release.env + echo "iOS Shorebird release version: $RELEASE_VERSION" + if ! shorebird releases list --platform=ios --json > build/shorebird/release_info.json 2> build/shorebird/release_info.err; then + echo "ERROR: could not list existing Shorebird releases." + if [ -s build/shorebird/release_info.err ]; then + sed 's/^/stderr: /' build/shorebird/release_info.err + fi + if [ -s build/shorebird/release_info.json ]; then + sed 's/^/stdout: /' build/shorebird/release_info.json + fi + exit 1 + fi + ruby scripts/shorebird_release_preflight.rb build/shorebird/release_info.json ios "$RELEASE_VERSION" + - &build_apk + name: Build Android APK + script: | + # Deliberately NOT a Shorebird release. Shorebird has no + # --split-per-abi; its Android artifact is either an AAB or one + # universal APK. These per-ABI APKs go to Zapstore / direct download, + # so those installs are not patchable. Play (the AAB below) is. + flutter build apk --release --split-per-abi --build-number=$PROJECT_BUILD_NUMBER \ + --dart-define-from-file=build/shorebird/dart_defines.json + - &build_aab + name: Build Android AAB (Shorebird release) + script: | + set -euo pipefail + # Use the latest Play versionCode as the primary source of truth. + . build/shorebird/android_release.env + BUILD_NAME=$SHOREBIRD_BUILD_NAME + BUILD_NUMBER=$SHOREBIRD_BUILD_NUMBER + echo "Android build number: $BUILD_NUMBER (release: $SHOREBIRD_RELEASE_VERSION)" + # Shorebird wraps `flutter build appbundle` with its own engine so the + # release carries the updater. A plain `flutter build` here would ship + # a permanently unpatchable version to Play. + shorebird release android --flutter-version=$FLUTTER_VERSION --build-name=$BUILD_NAME --build-number=$BUILD_NUMBER \ + --dart-define-from-file=build/shorebird/dart_defines.json \ + --public-key-path=build/shorebird/patch_public_key.pem + - &confirm_patch + name: Confirm patch to production + script: | + # Runs first so a mis-clicked workflow fails in seconds rather than + # after pod install / Gradle setup. + if [ "${{ inputs.CONFIRM_PATCH }}" != "YES" ]; then + echo "Refusing to patch: CONFIRM_PATCH is '${{ inputs.CONFIRM_PATCH }}', expected YES." + echo + echo "A patch reaches every install of release ${{ inputs.RELEASE_VERSION }}" + echo "within about one app launch, including App Store and Play" + echo "production users. There is no store review; rollback exists but" + echo "is an emergency mitigation, not a substitute for validation." + echo + echo "Re-run with CONFIRM_PATCH=YES if that is what you intend." + exit 1 + fi + if ! printf '%s\n' "${{ inputs.RELEASE_VERSION }}" | grep -Eq '^[0-9]+\.[0-9]+\.[0-9]+\+[0-9]+$'; then + echo "Refusing to patch: RELEASE_VERSION must look like 1.0.9+247." + echo "Got: '${{ inputs.RELEASE_VERSION }}'" + exit 1 + fi + echo "Patching release ${{ inputs.RELEASE_VERSION }} with DEFAULT_ENV=${{ inputs.DEFAULT_ENV }}." + - &patch_android + name: Patch Android (Shorebird) + script: | + # No --flutter-version: a patch is compiled against the Flutter version + # its release was built with, which Shorebird resolves from the release. + # The dart-defines must match the release's or the patch changes app + # behaviour beyond the code fix. Shorebird refuses to patch when it + # detects native or asset diffs; do NOT add --allow-native-diffs or + # --allow-asset-diffs to force past that — cut a new release instead. + shorebird patch android --release-version=${{ inputs.RELEASE_VERSION }} --track=staging \ + --dart-define-from-file=build/shorebird/dart_defines.json \ + --private-key-path=build/shorebird/patch_private_key.pem - &build_macos name: Build macOS app script: | @@ -260,44 +552,28 @@ definitions: "${ARTIFACTS[@]}" fi - &build_ios - name: Build and sign iOS + name: Build and sign iOS (Shorebird release) script: | - # App Store Connect is the source of truth, mirroring the Play lookup in - # the AAB step. Apple rejects any upload whose build number does not - # exceed the last one accepted for the version train, and it does so - # *after* the upload succeeds — so a stale number costs a full build and - # an emailed rejection rather than a fast failure. - APP_STORE_APP_ID=6747959501 # co.openvine.app - ASC_LOOKUP_LOG=$(mktemp) - trap 'rm -f "$ASC_LOOKUP_LOG"' EXIT - if ! LATEST_IOS_BUILD_NUMBER=$(app-store-connect get-latest-build-number "$APP_STORE_APP_ID" 2>"$ASC_LOOKUP_LOG"); then - LATEST_IOS_BUILD_NUMBER= - fi - case "$LATEST_IOS_BUILD_NUMBER" in - ''|*[!0-9]*) - echo "ERROR: could not read the latest App Store Connect build number." - if [ -s "$ASC_LOOKUP_LOG" ]; then - echo " App Store Connect lookup output:" - sed 's/^/ /' "$ASC_LOOKUP_LOG" - fi - echo " Refusing to guess a build number Apple may reject after upload." - exit 1 - ;; - *) - NEXT_IOS_BUILD_NUMBER=$((LATEST_IOS_BUILD_NUMBER + 1)) - BUILD_NUMBER=$NEXT_IOS_BUILD_NUMBER - ;; - esac - echo "iOS build number: $BUILD_NUMBER (App Store Connect latest: ${LATEST_IOS_BUILD_NUMBER:-unknown})" - flutter build ipa --release --build-number=$BUILD_NUMBER \ - --dart-define=ZENDESK_APP_ID=$ZENDESK_APP_ID \ - --dart-define=ZENDESK_CLIENT_ID=$ZENDESK_CLIENT_ID \ - --dart-define=ZENDESK_URL=$ZENDESK_URL \ - --dart-define=DEFAULT_ENV=${{ inputs.DEFAULT_ENV }} \ - --dart-define=PROOFMODE_SIGNING_SERVER_ENDPOINT=$PROOFMODE_SIGNING_SERVER_ENDPOINT \ - --dart-define=PROOFMODE_SIGNING_SERVER_TOKEN=$PROOFMODE_SIGNING_SERVER_TOKEN \ - --dart-define=SUPPORTERS_API_BASE_URL=$SUPPORTERS_API_BASE_URL \ - --dart-define=FF_DIVINE_SUPPORTERS=$FF_DIVINE_SUPPORTERS \ + set -euo pipefail + . build/shorebird/ios_release.env + BUILD_NAME=$SHOREBIRD_BUILD_NAME + BUILD_NUMBER=$SHOREBIRD_BUILD_NUMBER + echo "iOS build number: $BUILD_NUMBER (release: $SHOREBIRD_RELEASE_VERSION)" + # Shorebird wraps `flutter build ipa` with its own engine so the release + # carries the updater. A plain `flutter build` here would ship a + # permanently unpatchable version to TestFlight and the App Store. + shorebird release ios --flutter-version=$FLUTTER_VERSION --build-name=$BUILD_NAME --build-number=$BUILD_NUMBER \ + --dart-define-from-file=build/shorebird/dart_defines.json \ + --public-key-path=build/shorebird/patch_public_key.pem \ + --export-options-plist=/Users/builder/export_options.plist + - &patch_ios + name: Patch iOS (Shorebird) + script: | + # See the Android patch step for why there is no --flutter-version and + # why the dart-defines must match the release's. + shorebird patch ios --release-version=${{ inputs.RELEASE_VERSION }} --track=staging \ + --dart-define-from-file=build/shorebird/dart_defines.json \ + --private-key-path=build/shorebird/patch_private_key.pem \ --export-options-plist=/Users/builder/export_options.plist - &upload_dsyms_to_crashlytics name: Upload dSYMs to Firebase Crashlytics @@ -582,14 +858,19 @@ workflows: - "NO" - "YES" environment: - flutter: 3.44.0 + flutter: 3.44.9 xcode: latest cocoapods: default groups: - zendesk_credentials - proofmode_credentials - github_credentials + - shorebird_credentials vars: + # Must match `flutter:` above. Shorebird ships its own Flutter, so the + # release is only reproducible if we pin it to the same version + # Codemagic provisioned. + FLUTTER_VERSION: 3.44.9 BUNDLE_ID: co.openvine.app IOS_EXTENSION_BUNDLE_IDS: >- co.openvine.app.NotificationServiceExtension @@ -600,18 +881,23 @@ workflows: cache: cache_paths: - $HOME/Library/Caches/CocoaPods + - $HOME/.shorebird - $HOME/.pub-cache triggering: events: [] scripts: + - *shorebird_install - *check_signing_endpoint - *check_supporters_config - *check_ios_extension_signing_contract + - *write_shorebird_public_key + - *preflight_shorebird_ios_release - *setup_code_signing_xcode - *clean_ios_workspace_state - *enable_spm - *prepare_ios_spm_packages - *pod_install + - *write_shorebird_dart_defines - *build_ios - *upload_dsyms_to_crashlytics - *publish_github_release @@ -647,7 +933,7 @@ workflows: - TEST - POC environment: - flutter: 3.44.0 + flutter: 3.44.9 xcode: latest cocoapods: default groups: @@ -696,27 +982,36 @@ workflows: - "NO" - "YES" environment: - flutter: 3.44.0 + flutter: 3.44.9 java: 17 groups: - zendesk_credentials - google_play_credentials - proofmode_credentials - github_credentials + - shorebird_credentials + vars: + # Must match `flutter:` above — see the iOS workflow for why. + FLUTTER_VERSION: 3.44.9 android_signing: - divine_keystore cache: cache_paths: - $HOME/.gradle/caches + - $HOME/.shorebird - $HOME/.pub-cache triggering: events: [] scripts: + - *shorebird_install - *check_signing_endpoint - *check_supporters_config - *enable_spm - *setup_local_properties - *configure_gradle_memory + - *write_shorebird_public_key + - *preflight_shorebird_android_release + - *write_shorebird_dart_defines - *build_apk - *build_aab - *publish_github_release @@ -735,6 +1030,162 @@ workflows: # release in the Play Console after completing that declaration. submit_as_draft: true + # Patch workflows ship a Dart-only fix to an already-released build over the + # air. They publish to Shorebird, not to Play or the App Store, and are + # manual-trigger only. The workflows publish to the Shorebird staging track; + # promoting to stable reaches users within one app launch, so it should be a + # deliberate act, never a side effect of a push. + # + # A patch can only target a release that was cut by `shorebird release` + # (the AAB and IPA steps above). Per-ABI APKs from the Zapstore path are + # plain flutter builds and cannot be patched. + ios-patch: + name: iOS Patch (Shorebird) + working_directory: mobile + max_build_duration: 60 + instance_type: mac_mini_m2 + integrations: + app_store_connect: API key for Codemagic # Setup in Codemagic UI + inputs: + CONFIRM_PATCH: + description: >- + Must be YES to proceed. Guards against a mis-clicked workflow — a + patch ships with no store review and rollback is only an emergency + mitigation. + type: choice + default: "NO" + options: + - "NO" + - "YES" + RELEASE_VERSION: + description: >- + Release to patch, exactly as Shorebird lists it, including the build + number (e.g. 1.0.9+247). Get it from `shorebird releases list` or the + Shorebird console — a value with no matching release fails the build. + type: string + RELEASE_COMMIT: + description: >- + Git commit that produced the target store release. The workflow + prints every commit and Dart file that will be included in the patch + beyond this release commit. + type: string + DEFAULT_ENV: + description: >- + Must match the value the target release was built with. A mismatch + silently repoints patched installs at a different environment. + type: choice + default: PRODUCTION + options: + - PRODUCTION + - STAGING + - TEST + - POC + environment: + flutter: 3.44.9 + xcode: latest + cocoapods: default + groups: + - zendesk_credentials + - proofmode_credentials + - shorebird_credentials + vars: + BUNDLE_ID: co.openvine.app + IOS_EXTENSION_BUNDLE_IDS: >- + co.openvine.app.NotificationServiceExtension + co.openvine.app.CameraQuickActionWidget + ios_signing: + distribution_type: app_store + bundle_identifier: co.openvine.app + cache: + cache_paths: + - $HOME/Library/Caches/CocoaPods + - $HOME/.shorebird + - $HOME/.pub-cache + triggering: + events: [] + scripts: + - *confirm_patch + - *prepare_patch_source + - *shorebird_install + - *check_signing_endpoint + - *check_supporters_config + - *check_ios_extension_signing_contract + - *write_shorebird_private_key + - *setup_code_signing_xcode + - *clean_ios_workspace_state + - *enable_spm + - *prepare_ios_spm_packages + - *pod_install + - *write_shorebird_dart_defines + - *patch_ios + + android-patch: + name: Android Patch (Shorebird) + working_directory: mobile + max_build_duration: 60 + instance_type: mac_mini_m4 + inputs: + CONFIRM_PATCH: + description: >- + Must be YES to proceed. Guards against a mis-clicked workflow — a + patch ships with no store review and rollback is only an emergency + mitigation. + type: choice + default: "NO" + options: + - "NO" + - "YES" + RELEASE_VERSION: + description: >- + Release to patch, exactly as Shorebird lists it, including the build + number (e.g. 1.0.9+247). Get it from `shorebird releases list` or the + Shorebird console — a value with no matching release fails the build. + type: string + RELEASE_COMMIT: + description: >- + Git commit that produced the target store release. The workflow + prints every commit and Dart file that will be included in the patch + beyond this release commit. + type: string + DEFAULT_ENV: + description: >- + Must match the value the target release was built with. A mismatch + silently repoints patched installs at a different environment. + type: choice + default: PRODUCTION + options: + - PRODUCTION + - STAGING + - TEST + - POC + environment: + flutter: 3.44.9 + java: 17 + groups: + - zendesk_credentials + - proofmode_credentials + - shorebird_credentials + android_signing: + - divine_keystore + cache: + cache_paths: + - $HOME/.gradle/caches + - $HOME/.shorebird + - $HOME/.pub-cache + triggering: + events: [] + scripts: + - *confirm_patch + - *prepare_patch_source + - *shorebird_install + - *check_signing_endpoint + - *check_supporters_config + - *setup_local_properties + - *configure_gradle_memory + - *write_shorebird_private_key + - *write_shorebird_dart_defines + - *patch_android + macos-build: name: macOS Build working_directory: mobile @@ -758,7 +1209,7 @@ workflows: - "NO" - "YES" environment: - flutter: 3.44.0 + flutter: 3.44.9 xcode: latest cocoapods: default groups: @@ -796,7 +1247,7 @@ workflows: max_build_duration: 25 instance_type: mac_mini_m2 environment: - flutter: 3.44.0 + flutter: 3.44.9 # Pinned alongside MAESTRO_VERSION. The compatibility contract has two # sides: 2.1.0 cannot drive Xcode 26.x, and `latest` floating means a # Codemagic image bump can break the suite with no commit on our side — @@ -868,7 +1319,7 @@ workflows: max_build_duration: 60 instance_type: linux_x2 environment: - flutter: 3.44.0 + flutter: 3.44.9 java: 17 groups: - zendesk_credentials diff --git a/mobile/docs/SHOREBIRD_CODE_PUSH.md b/mobile/docs/SHOREBIRD_CODE_PUSH.md new file mode 100644 index 0000000000..7f2f37debc --- /dev/null +++ b/mobile/docs/SHOREBIRD_CODE_PUSH.md @@ -0,0 +1,257 @@ +# Shorebird Code Push + +Shorebird ships Dart-only fixes to already-released builds over the air. A +patch reaches users on their next app launch without a store submission. + +Read this before changing anything in `codemagic.yaml` that touches a store +artifact, and before shipping a patch. + +## The one rule that matters + +**A build is patchable only if it was produced by `shorebird release`.** That +command links the Shorebird updater into the engine; a plain `flutter build` +does not. If a store-bound artifact is ever built with `flutter build`, that +version is permanently unpatchable — there is no way to fix it after the fact +short of shipping a new store release. + +This is why the Play AAB and App Store IPA steps in `codemagic.yaml` call +`shorebird release`. Do not "simplify" them back to `flutter build`. + +## What can and cannot be patched + +| Change | Patchable | +|---|---| +| Dart code — logic, UI, strings | Yes | +| Native code (Kotlin/Swift, plugins with native parts) | No | +| Assets bundled at build time | No | +| Adding or upgrading a dependency with native code | No | +| Flutter or engine version | No | + +Shorebird detects native and asset diffs and **refuses** to build the patch. +That refusal is a correctness guard: a patch that disagrees with the binary it +lands on produces crashes that are extremely hard to diagnose in the field. + +`--allow-native-diffs` and `--allow-asset-diffs` override the guard. Do not add +them. If a fix needs native changes, it needs a store release. + +## How Divine is set up + +The app is `mobile/shorebird.yaml` → `app_id`, owned by the **Divine** +organization in Shorebird (not an individual's account). + +### Privacy disclosure + +Shorebird checks for patches when the app launches. The request includes a +random identifier unique to that app installation, the app, release, and patch +versions, update channel, platform, and device architecture. Shorebird also +records patch download, installation, and failure status. This information is +used to deliver updates, diagnose update failures, and produce aggregated +update and active-install analytics. The installation identifier is not an +advertising identifier and is not used for advertising or cross-app tracking. + +Keep the public privacy policy and store disclosures aligned with this +behavior. In App Store Connect, disclose Device ID, Product Interaction, and +Other Diagnostic Data for App Functionality and Analytics, without tracking. +Mark them as not linked only while neither Divine nor Shorebird joins the +installation identifier to Divine account data. In Google Play, disclose Device +or other IDs, App interactions, and Diagnostics as required collection for App +functionality and Analytics. Mark the Google Play data as not shared only while +Shorebird is contractually acting as Divine's service provider or processor. +Do not finalize that answer until the DPA is executed and the privacy review +confirms that its terms cover data relating to every intended user, including +minor users. + +| Artifact | Built by | Patchable | +|---|---|---| +| Play AAB | `shorebird release android` | Yes | +| App Store IPA | `shorebird release ios` | Yes | +| Zapstore / GitHub per-ABI APKs | `flutter build apk --split-per-abi` | **No** — deliberate | +| macOS DMG | `flutter build macos` | No — not wired | + +The Zapstore APKs are an intentional exception: Shorebird has no +`--split-per-abi`, and its universal APK would inflate the download for every +sideload user. Those installs cannot receive patches. Play and App Store +installs can. + +`auto_update` is left at its default (on), so patches apply in the background +on launch. `shorebird_code_push` is a runtime dependency. There is no in-app UI +for controlling patch state, but startup records patch availability and the +current patch number in logs and Crashlytics custom keys. Use those values when +triaging crashes that may be specific to a code-push patch. + +### Flutter version + +Shorebird ships its own Flutter, so `FLUTTER_VERSION` in `codemagic.yaml` +duplicates the workflow's `flutter:` setting and is passed to +`shorebird release --flutter-version`. **Keep the two in sync**, and when +bumping Flutter, confirm the target is in `shorebird flutter versions list` +first — Shorebird supports a subset. + +The current 3.44.9 pin is deliberate. Shorebird's 3.44.0 engine crashed at +launch in the Dart async FFI path used by `cupertino_http`, so the repo and all +CI workflows moved together to the refreshed 3.44.9 Shorebird engine. Do not +move the pin back independently in one workflow. + +Patch commands take no `--flutter-version`. A patch is compiled against +whatever version its release used, which Shorebird resolves from the release. + +## Cutting a release + +Run `ios-build` or `android-build` in Codemagic as usual. They now produce +Shorebird releases as a side effect of building the store artifact — no extra +step, no change to how you ship. + +Before starting a store build, update `mobile/pubspec.yaml` to the intended +marketing version. The iOS preflight reads the latest approved App Store +version and refuses a candidate that is equal or lower before Shorebird builds +or records a release. Increasing only the build number cannot reopen a closed +App Store version train. + +Publishing behaviour is unchanged: iOS stops at TestFlight +(`submit_to_app_store: false`, internal groups), Android uploads to Play as a +draft. Promotion to production stays manual in both stores. + +Release commands pass `--public-key-path`, so every new store binary only +accepts signed Shorebird patches. A release built without the public key cannot +be retrofitted later; cut a new store release instead. + +**Record the version and build number of every release you promote** — e.g. +`1.0.9+247` — and the git commit that produced it. A future patch needs both. + +## Shipping a patch + +1. Land the Dart fix on `main` through the normal PR process. A patch is built + from the current checkout, so the fix must be merged first. +1. Identify the target release: `shorebird patch` needs the version of the + build that is actually live. `shorebird releases list`, or the Shorebird + console, shows what exists. +1. Identify the `RELEASE_COMMIT` that produced that store release. The patch + workflow refuses a release commit that is not an ancestor of the current + checkout, then prints every commit and Dart file included after that commit. + Read that list before treating the patch as safe: pure Dart merged since the + release is exactly what Shorebird can ship. + If the diff includes native, asset, dependency, or Shorebird config changes, + the workflow refuses to patch and tells you to cut a normal store release. +1. Run `ios-patch` or `android-patch` in Codemagic with: + - `CONFIRM_PATCH` = `YES` (defaults to `NO`; the build aborts otherwise) + - `RELEASE_VERSION` = the exact release string, e.g. `1.0.9+247` + - `RELEASE_COMMIT` = the git commit that produced the target release + - `DEFAULT_ENV` = **the same value the release was built with** +1. The workflow publishes to Shorebird's `staging` track, not directly to + `stable`. +1. Validate the staged patch with `shorebird preview --track staging + --release-version ` or the Shorebird console. +1. Promote the exact patch after validation: + + ``` + shorebird patches set-track --release --patch --track stable + ``` + + `shorebird patches promote --release-version --patch-number ` + is the deprecated legacy shorthand in the pinned CLI. Do not use it in new + automation. +1. Both platforms need their own patch run. There is no combined workflow. + +`DEFAULT_ENV` is the easiest thing to get wrong. A mismatch does not fail the +build — it silently repoints patched installs at a different environment. + +### A patch targets a release version, not a channel + +TestFlight build `1.0.9+247` and App Store build `1.0.9+247` are the same +Shorebird release. There is no way to patch "just the TestFlight build". If +that version was promoted to the App Store, patching it reaches production +users too once the patch is promoted to `stable`, within about one app launch, +with no store review. + +Treat every patch as a production deploy. + +### Rollback is recovery, not validation + +Shorebird supports patch rollback and on-device rollback from failed patches. +That reduces the worst-case blast radius, but it does not make patching casual: +users can still receive the bad patch before rollback, and rollback does not +replace review, staging validation, signing, or source-diff checks. + +## Credentials + +CI authenticates with `SHOREBIRD_TOKEN`, held in the `shorebird_credentials` +environment variable group in Codemagic and marked Secret. It is an API key +from the Shorebird console (**Account → API Keys**); `shorebird login:ci` no +longer exists. + +The same group holds patch-signing key material: + +- `SHOREBIRD_PATCH_PUBLIC_KEY` — public PEM passed to `shorebird release` +- `SHOREBIRD_PATCH_PRIVATE_KEY` — secure private PEM passed to `shorebird patch` + +Store both keys as single-line values with literal `\n` sequences between PEM +lines. CI decodes and validates that envelope before invoking Shorebird. Release +jobs only materialize the public key; patch jobs only materialize the private +key. + +The token is currently generated from an individual's account. Moving it to a +Divine service identity is tracked in #7200. + +## Local setup + +Installing the CLI: + +``` +if [ ! -x "$HOME/.shorebird/bin/shorebird" ]; then + curl --proto '=https' --tlsv1.2 https://raw.githubusercontent.com/shorebirdtech/install/main/install.sh -sSf | bash +fi +``` + +The installer refuses to overwrite an existing installation. Existing installs +do not need to rerun it; use `bash -s -- --force` only when deliberately +replacing the installed CLI. + +Then `shorebird login`. You need access to the Divine organization; membership +is managed in the Shorebird console. + +`shorebird doctor` from `mobile/` reports one known issue — +`ios/Podfile.lock` is not tracked in source control (`mobile/.gitignore`). +It is worth revisiting, since patches cannot change native code and a release +and its patches should resolve identical pods. + +You cannot run `shorebird release android` or `shorebird patch android` on a +machine without a JDK and a complete Android SDK. Android release and patch +builds run on Codemagic, which provisions both. That is also the correct place +for them: a patch should be compiled in the same environment as the release it +targets. + +### Known gotchas + +**A release build is not idempotent after `shorebird release` succeeds.** If a +later upload or symbol step fails, the store build-number lookup can derive the +same version again, but Shorebird will refuse to create it twice. The preflight +stops that retry before the long build. Delete the failed Shorebird release or +bump the store build number before rerunning; do not bypass the preflight. + +**App Store Connect rejects builds for an approved marketing version.** Once +Apple approves a version such as `1.0.20`, its pre-release train is closed and +a higher build number does not reopen it. Bump the version in +`mobile/pubspec.yaml`; the iOS preflight checks this before invoking Shorebird. + +**`shorebird init` fails with "Unable to initialize gradlew".** `gradlew` is +gitignored, so a fresh worktree has no wrapper, and `init` calls it directly +rather than through `flutter build`. See #7201. Workaround: + +``` +echo "flutter.sdk=$HOME/flutter" > mobile/android/local.properties +cd mobile && flutter build apk --config-only # injects the wrapper +``` + +`init` also needs a JDK for that call, which a machine set up only for iOS +work will not have. + +**`shorebird init` invents a `divineuitests` flavor.** Its iOS detection treats +every shared Xcode scheme that is not `Runner` as a product flavor, and picks up +the `DivineUITests` test target. The app has no product flavors — if a re-init +ever writes a `flavors:` block into `shorebird.yaml`, delete it. + +## Reference + +- [Shorebird docs](https://docs.shorebird.dev) +- [Codemagic integration](https://docs.shorebird.dev/code-push/ci/codemagic/) +- `codemagic.yaml` — release and patch workflows, setup steps in the header diff --git a/mobile/mise.toml b/mobile/mise.toml index b94819cb4c..71b8a76596 100644 --- a/mobile/mise.toml +++ b/mobile/mise.toml @@ -1,5 +1,5 @@ [tools] -flutter = "3.44.0" +flutter = "3.44.9" [tasks.local_up] description = "Start all local E2E services" diff --git a/mobile/packages/caption_generator/android/build.gradle.kts b/mobile/packages/caption_generator/android/build.gradle.kts index c3685ef664..38ccaafbdd 100644 --- a/mobile/packages/caption_generator/android/build.gradle.kts +++ b/mobile/packages/caption_generator/android/build.gradle.kts @@ -15,7 +15,7 @@ val flutterRoot = providers.environmentVariable("FLUTTER_ROOT") .orElse(providers.environmentVariable("FLUTTER_HOME")) .orNull ?: throw GradleException("FLUTTER_ROOT must be set to compile caption_generator") -val flutterEmbeddingJar = +val flutterDebugEmbeddingJar = file("$flutterRoot/bin/cache/artifacts/engine/android-arm64/flutter.jar") android { @@ -49,8 +49,8 @@ android { } dependencies { - compileOnly(files(flutterEmbeddingJar)) - testImplementation(files(flutterEmbeddingJar)) + compileOnly(files(flutterDebugEmbeddingJar)) + testImplementation(files(flutterDebugEmbeddingJar)) testImplementation("org.jetbrains.kotlin:kotlin-test") testImplementation("org.mockito:mockito-core:5.0.0") testImplementation("org.robolectric:robolectric:4.14.1") diff --git a/mobile/pubspec.yaml b/mobile/pubspec.yaml index 058fbae212..844256a185 100644 --- a/mobile/pubspec.yaml +++ b/mobile/pubspec.yaml @@ -16,7 +16,7 @@ publish_to: "none" # Remove this line if you wish to publish to pub.dev # https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html # In Windows, build-name is used as the major, minor, and patch parts # of the product and file versions while build-number is used as the build suffix. -version: 1.0.20+820 +version: 1.0.21+820 environment: sdk: ^3.12.0 diff --git a/mobile/scripts/ios_store_version_preflight.rb b/mobile/scripts/ios_store_version_preflight.rb new file mode 100644 index 0000000000..9d78ab0d6a --- /dev/null +++ b/mobile/scripts/ios_store_version_preflight.rb @@ -0,0 +1,37 @@ +#!/usr/bin/env ruby +# frozen_string_literal: true + +require 'json' + +json_path, candidate_version = ARGV +unless json_path && candidate_version + abort('usage: ios_store_version_preflight.rb ') +end + +def numeric_version(value, label) + unless value.is_a?(String) && value.match?(/\A\d+(?:\.\d+){0,2}\z/) + abort("ERROR: invalid #{label} #{value.inspect}; expected one to three numeric components.") + end + + value.split('.').map(&:to_i).fill(0, value.count('.') + 1...3) +end + +payload = JSON.parse(File.read(json_path)) +unless payload.is_a?(Hash) + abort('ERROR: expected a JSON object from App Store Connect.') +end + +approved_version = payload['version'] +abort('ERROR: App Store Connect response is missing version.') unless approved_version + +candidate_parts = numeric_version(candidate_version, 'candidate version') +approved_parts = numeric_version(approved_version, 'approved App Store version') + +unless (candidate_parts <=> approved_parts) == 1 + abort( + "ERROR: candidate version #{candidate_version} must be newer than approved App Store version " \ + "#{approved_version}. Bump mobile/pubspec.yaml before cutting a Shorebird release.", + ) +end + +puts "Candidate version #{candidate_version} is newer than approved App Store version #{approved_version}." diff --git a/mobile/scripts/shorebird_release_preflight.rb b/mobile/scripts/shorebird_release_preflight.rb new file mode 100644 index 0000000000..f8664b8874 --- /dev/null +++ b/mobile/scripts/shorebird_release_preflight.rb @@ -0,0 +1,46 @@ +#!/usr/bin/env ruby +# frozen_string_literal: true + +require 'json' + +json_path, platform, release_version = ARGV +unless json_path && platform && release_version + abort('usage: shorebird_release_preflight.rb ') +end + +payload = JSON.parse(File.read(json_path)) + +unless payload['status'] == 'success' + error = payload['error'] || {} + code = error['code'] || 'unknown_error' + message = error['message'] || 'no error message' + abort("ERROR: Shorebird releases list failed (#{code}): #{message}") +end + +data = payload['data'] +releases = data && data['releases'] +unless releases.is_a?(Array) + abort('ERROR: Shorebird releases list JSON is missing releases array at data.releases.') +end + +release = releases.find { |item| item['version'] == release_version } +unless release + puts "No existing Shorebird #{platform} release found for #{release_version}; continuing." + exit 0 +end + +platform_statuses = release['platform_statuses'] +unless platform_statuses.is_a?(Hash) + abort("ERROR: Shorebird release #{release_version} has unreadable platform_statuses.") +end + +status = platform_statuses[platform] +unless status + puts "No existing Shorebird #{platform} release found for #{release_version}; continuing." + exit 0 +end + +abort( + "ERROR: #{platform} Shorebird release #{release_version} already exists with status #{status}.\n" \ + 'Bump the build number/version or delete the failed Shorebird release before retrying.', +) diff --git a/mobile/shorebird.yaml b/mobile/shorebird.yaml index dbbe1acb69..2049a82a03 100644 --- a/mobile/shorebird.yaml +++ b/mobile/shorebird.yaml @@ -5,7 +5,7 @@ # Your app_id is the unique identifier assigned to your app. # It is used to identify your app when requesting patches from Shorebird's servers. # It is not a secret and can be shared publicly. -app_id: 5a90f08c-3325-4805-aa02-9f056115b49b +app_id: 73254661-21d7-4df0-90f3-265c044d7c9a # auto_update controls if Shorebird should automatically update in the background on launch. # If auto_update: false, you will need to use package:shorebird_code_push to trigger updates.