From 00c149a71bf70f2ef95f0e9fe0264258757ead03 Mon Sep 17 00:00:00 2001 From: Erick Dronski Date: Tue, 6 Oct 2026 17:56:51 -0400 Subject: [PATCH 1/7] Bump ruby/setup-ruby to v1.323.0 to stay on a maintained pin Carries Dependabot PR #48 (github-actions group) onto this branch so the SHA-pinned setup-ruby action in every workflow moves from v1.321.0 to v1.323.0 (984c0c8). Merging this lets Dependabot close #48 as applied. Co-Authored-By: Claude Opus 5.5 --- .github/workflows/app-store-preflight.yml | 2 +- .github/workflows/ios-testflight.yml | 4 ++-- .github/workflows/release-tooling-tests.yml | 2 +- .github/workflows/screenshots-upload.yml | 2 +- .github/workflows/screenshots.yml | 2 +- 5 files changed, 6 insertions(+), 6 deletions(-) diff --git a/.github/workflows/app-store-preflight.yml b/.github/workflows/app-store-preflight.yml index 4f35452..d43f7b9 100644 --- a/.github/workflows/app-store-preflight.yml +++ b/.github/workflows/app-store-preflight.yml @@ -63,7 +63,7 @@ jobs: fetch-depth: 0 - name: Ruby + Fastlane (cached) - uses: ruby/setup-ruby@95ef2b042f9d7a56d8268cba8559e2842e2ad01b # v1 + uses: ruby/setup-ruby@984c0c890880bbf811283d6f09c4607c62d210a4 # v1 with: ruby-version: "3.2" bundler-cache: true diff --git a/.github/workflows/ios-testflight.yml b/.github/workflows/ios-testflight.yml index 7e19fc7..0023a5c 100644 --- a/.github/workflows/ios-testflight.yml +++ b/.github/workflows/ios-testflight.yml @@ -57,7 +57,7 @@ jobs: run: node scripts/verify-edge-function-contracts.mjs - name: Ruby + Fastlane (cached) - uses: ruby/setup-ruby@95ef2b042f9d7a56d8268cba8559e2842e2ad01b # v1 + uses: ruby/setup-ruby@984c0c890880bbf811283d6f09c4607c62d210a4 # v1 with: ruby-version: "3.2" bundler-cache: true @@ -158,7 +158,7 @@ jobs: run: xcodegen generate - name: Ruby + Fastlane (cached) - uses: ruby/setup-ruby@95ef2b042f9d7a56d8268cba8559e2842e2ad01b # v1 + uses: ruby/setup-ruby@984c0c890880bbf811283d6f09c4607c62d210a4 # v1 with: ruby-version: "3.2" bundler-cache: true diff --git a/.github/workflows/release-tooling-tests.yml b/.github/workflows/release-tooling-tests.yml index 1907b6a..42b29ed 100644 --- a/.github/workflows/release-tooling-tests.yml +++ b/.github/workflows/release-tooling-tests.yml @@ -18,7 +18,7 @@ jobs: uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - name: Ruby - uses: ruby/setup-ruby@95ef2b042f9d7a56d8268cba8559e2842e2ad01b # v1 + uses: ruby/setup-ruby@984c0c890880bbf811283d6f09c4607c62d210a4 # v1 with: ruby-version: "3.2" diff --git a/.github/workflows/screenshots-upload.yml b/.github/workflows/screenshots-upload.yml index be6b388..4586c7d 100644 --- a/.github/workflows/screenshots-upload.yml +++ b/.github/workflows/screenshots-upload.yml @@ -39,7 +39,7 @@ jobs: fetch-depth: 0 - name: Ruby + Fastlane (cached) - uses: ruby/setup-ruby@95ef2b042f9d7a56d8268cba8559e2842e2ad01b # v1 + uses: ruby/setup-ruby@984c0c890880bbf811283d6f09c4607c62d210a4 # v1 with: ruby-version: "3.2" bundler-cache: true diff --git a/.github/workflows/screenshots.yml b/.github/workflows/screenshots.yml index 94b37b1..ae0f24a 100644 --- a/.github/workflows/screenshots.yml +++ b/.github/workflows/screenshots.yml @@ -42,7 +42,7 @@ jobs: run: xcrun simctl list devices available | grep -Ei "iphone|ipad" || true - name: Ruby + Fastlane (cached) - uses: ruby/setup-ruby@95ef2b042f9d7a56d8268cba8559e2842e2ad01b # v1 + uses: ruby/setup-ruby@984c0c890880bbf811283d6f09c4607c62d210a4 # v1 with: ruby-version: "3.2" bundler-cache: true From b5ce6f484b5f78ffcba9894fdb5000933481507a Mon Sep 17 00:00:00 2001 From: Erick Dronski Date: Tue, 6 Oct 2026 17:57:05 -0400 Subject: [PATCH 2/7] Run CI on Ruby 3.4 so the patched fastlane bundle can install Ruby 3.2 reached end of life in March 2026, and the current fastlane dependency tree no longer installs on it: excon 1.7.1 and rbs 4.2.0 require Ruby >= 3.3. That is exactly why Dependabot's fastlane 2.240.0 PR failed CI at "Ruby + Fastlane (cached)". Move every workflow to Ruby 3.4 so the security upgrade in the next commit can install and run. Only the interpreter version changes. Triggers, environments, secrets and the manual TestFlight/App Store gates are untouched. Co-Authored-By: Claude Opus 5.5 --- .github/workflows/app-store-preflight.yml | 2 +- .github/workflows/ios-testflight.yml | 4 ++-- .github/workflows/release-tooling-tests.yml | 2 +- .github/workflows/screenshots-upload.yml | 2 +- .github/workflows/screenshots.yml | 2 +- 5 files changed, 6 insertions(+), 6 deletions(-) diff --git a/.github/workflows/app-store-preflight.yml b/.github/workflows/app-store-preflight.yml index d43f7b9..d6b1b13 100644 --- a/.github/workflows/app-store-preflight.yml +++ b/.github/workflows/app-store-preflight.yml @@ -65,7 +65,7 @@ jobs: - name: Ruby + Fastlane (cached) uses: ruby/setup-ruby@984c0c890880bbf811283d6f09c4607c62d210a4 # v1 with: - ruby-version: "3.2" + ruby-version: "3.4" bundler-cache: true - name: Validate release guards before App Store access diff --git a/.github/workflows/ios-testflight.yml b/.github/workflows/ios-testflight.yml index 0023a5c..90c5cb2 100644 --- a/.github/workflows/ios-testflight.yml +++ b/.github/workflows/ios-testflight.yml @@ -59,7 +59,7 @@ jobs: - name: Ruby + Fastlane (cached) uses: ruby/setup-ruby@984c0c890880bbf811283d6f09c4607c62d210a4 # v1 with: - ruby-version: "3.2" + ruby-version: "3.4" bundler-cache: true - name: Run Lore unit tests @@ -160,7 +160,7 @@ jobs: - name: Ruby + Fastlane (cached) uses: ruby/setup-ruby@984c0c890880bbf811283d6f09c4607c62d210a4 # v1 with: - ruby-version: "3.2" + ruby-version: "3.4" bundler-cache: true - name: Build and upload the validated commit to TestFlight diff --git a/.github/workflows/release-tooling-tests.yml b/.github/workflows/release-tooling-tests.yml index 42b29ed..771d260 100644 --- a/.github/workflows/release-tooling-tests.yml +++ b/.github/workflows/release-tooling-tests.yml @@ -20,7 +20,7 @@ jobs: - name: Ruby uses: ruby/setup-ruby@984c0c890880bbf811283d6f09c4607c62d210a4 # v1 with: - ruby-version: "3.2" + ruby-version: "3.4" - name: Run offline release-tooling tests run: ruby fastlane/spec/lore_release_tooling_test.rb diff --git a/.github/workflows/screenshots-upload.yml b/.github/workflows/screenshots-upload.yml index 4586c7d..31d3a21 100644 --- a/.github/workflows/screenshots-upload.yml +++ b/.github/workflows/screenshots-upload.yml @@ -41,7 +41,7 @@ jobs: - name: Ruby + Fastlane (cached) uses: ruby/setup-ruby@984c0c890880bbf811283d6f09c4607c62d210a4 # v1 with: - ruby-version: "3.2" + ruby-version: "3.4" bundler-cache: true - name: Upload screenshots to App Store Connect diff --git a/.github/workflows/screenshots.yml b/.github/workflows/screenshots.yml index ae0f24a..6f4292a 100644 --- a/.github/workflows/screenshots.yml +++ b/.github/workflows/screenshots.yml @@ -44,7 +44,7 @@ jobs: - name: Ruby + Fastlane (cached) uses: ruby/setup-ruby@984c0c890880bbf811283d6f09c4607c62d210a4 # v1 with: - ruby-version: "3.2" + ruby-version: "3.4" bundler-cache: true - name: Capture screenshots From d8f56950aa2402c0ce6a09e68a58c8b4762add3e Mon Sep 17 00:00:00 2001 From: Erick Dronski Date: Tue, 6 Oct 2026 17:57:05 -0400 Subject: [PATCH 3/7] Upgrade fastlane to 2.240.1 to patch rubyzip path traversal Resolves Dependabot alert #2 (GHSA-47m2-wp7j-p9vc / CVE-2026-85396, high): rubyzip < 3.4.0 can write outside the extraction directory. fastlane 2.239.0 pinned rubyzip < 3.0, which is why Dependabot's own rubyzip security update could not resolve. fastlane 2.240.1 moves to rubyzip >= 3.4, so the lockfile now resolves rubyzip 3.7.0. The Gemfile and Gemfile.lock are identical to Dependabot PR #49, so merging this lets Dependabot close #49 as applied. Verified locally that the locked bundle installs, loads fastlane 2.240.1 with rubyzip 3.7.0, and lists every lane. Co-Authored-By: Claude Opus 5.5 --- Gemfile | 2 +- Gemfile.lock | 98 +++++++++++++++++++++++++++------------------------- 2 files changed, 52 insertions(+), 48 deletions(-) diff --git a/Gemfile b/Gemfile index 8bdf9e9..b9d81cb 100644 --- a/Gemfile +++ b/Gemfile @@ -1,3 +1,3 @@ source "https://rubygems.org" -gem "fastlane", "2.239.0" +gem "fastlane", "2.240.1" diff --git a/Gemfile.lock b/Gemfile.lock index af9e4b1..056b752 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -8,8 +8,8 @@ GEM artifactory (3.0.17) atomos (0.1.3) aws-eventstream (1.4.0) - aws-partitions (1.1283.0) - aws-sdk-core (3.254.1) + aws-partitions (1.1290.0) + aws-sdk-core (3.257.0) aws-eventstream (~> 1, >= 1.3.0) aws-partitions (~> 1, >= 1.992.0) aws-sigv4 (~> 1.9) @@ -17,11 +17,12 @@ GEM bigdecimal jmespath (~> 1, >= 1.6.1) logger - aws-sdk-kms (1.130.0) - aws-sdk-core (~> 3, >= 3.254.0) + rexml (~> 3.4, >= 3.4.2) + aws-sdk-kms (1.132.0) + aws-sdk-core (~> 3, >= 3.256.0) aws-sigv4 (~> 1.5) - aws-sdk-s3 (1.229.0) - aws-sdk-core (~> 3, >= 3.254.1) + aws-sdk-s3 (1.232.1) + aws-sdk-core (~> 3, >= 3.256.0) aws-sdk-kms (~> 1) aws-sigv4 (~> 1.5) aws-sigv4 (1.12.1) @@ -29,7 +30,8 @@ GEM babosa (1.0.4) base64 (0.3.0) benchmark (0.5.0) - bigdecimal (4.1.2) + bigdecimal (4.1.3) + cgi (0.5.2) claide (1.1.0) colored (1.2) colored2 (3.1.2) @@ -39,13 +41,13 @@ GEM declarative (0.0.20) digest-crc (0.7.0) rake (>= 12.0.0, < 14.0.0) - domain_name (0.6.20260902) + domain_name (0.6.20260921) dotenv (2.8.1) emoji_regex (3.2.3) erb (6.0.7) - excon (1.6.0) + excon (1.7.1) logger - faraday (2.14.3) + faraday (2.14.4) faraday-net_http (>= 2.0, < 3.5) json logger @@ -61,7 +63,7 @@ GEM faraday-retry (2.4.0) faraday (~> 2.0) fastimage (2.4.1) - fastlane (2.239.0) + fastlane (2.240.1) CFPropertyList (>= 2.3, < 5.0.0) abbrev (~> 0.1) addressable (>= 2.9.0, < 3.0.0) @@ -71,6 +73,7 @@ GEM base64 (~> 0.2) benchmark (>= 0.1.0) bundler (>= 2.4.0, < 5.0.0) + cgi (~> 0.4) colored (~> 1.2) commander (~> 4.6) csv (~> 3.3) @@ -85,7 +88,7 @@ GEM fastimage (>= 2.1.0, < 3.0.0) fastlane-sirp (>= 1.1.0) gh_inspector (>= 1.1.2, < 2.0.0) - google-apis-androidpublisher_v3 (~> 0.99.0) + google-apis-androidpublisher_v3 (~> 0.99) google-apis-playcustomapp_v1 (~> 0.1) google-cloud-env (>= 1.6.0, < 2.4.0) google-cloud-storage (~> 1.31) @@ -103,8 +106,8 @@ GEM optparse (>= 0.1.1, < 1.0.0) ostruct (>= 0.1.0) plist (>= 3.1.0, < 4.0.0) - rubyzip (>= 2.0.0, < 3.0.0) - security (= 0.1.5) + rubyzip (>= 3.4.0, < 4.0.0) + security (~> 0.3) simctl (~> 1.6.3) terminal-notifier (>= 2.0.0, < 3.0.0) terminal-table (~> 4) @@ -116,7 +119,7 @@ GEM xcpretty-travis-formatter (>= 0.0.3, < 2.0.0) fastlane-sirp (1.1.0) gh_inspector (1.1.3) - google-apis-androidpublisher_v3 (0.99.0) + google-apis-androidpublisher_v3 (0.108.0) google-apis-core (>= 0.15.0, < 2.a) google-apis-core (1.2.5) addressable (~> 2.9) @@ -131,7 +134,7 @@ GEM google-apis-core (>= 0.15.0, < 2.a) google-apis-playcustomapp_v1 (0.18.0) google-apis-core (>= 0.15.0, < 2.a) - google-apis-storage_v1 (0.66.0) + google-apis-storage_v1 (0.67.0) google-apis-core (>= 0.15.0, < 2.a) google-cloud-core (1.9.0) google-cloud-env (>= 1.0, < 3.a) @@ -161,7 +164,7 @@ GEM highline (2.0.3) http-cookie (1.1.6) domain_name (~> 0.5) - io-console (0.9.2) + io-console (0.9.4) irb (1.18.0) pp (>= 0.6.0) prism (>= 1.3.0) @@ -169,12 +172,12 @@ GEM reline (>= 0.4.2) jmespath (1.6.2) json (2.21.2) - jwt (3.2.0) + jwt (3.3.0) base64 logger (1.7.0) mini_magick (4.13.2) mini_mime (1.1.5) - multi_json (1.21.1) + multi_json (1.21.2) multipart-post (2.4.1) mutex_m (0.3.0) nanaimo (0.4.0) @@ -193,7 +196,7 @@ GEM pstore (0.2.1) public_suffix (7.0.5) rake (13.4.2) - rbs (4.1.3) + rbs (4.2.0) logger prism (>= 1.6.0) tsort @@ -204,15 +207,15 @@ GEM tsort reline (0.7.0) io-console (~> 0.5) - representable (3.2.0) + representable (3.3.0) declarative (< 0.1.0) trailblazer-option (>= 0.1.1, < 0.2.0) uber (< 0.2.0) retriable (4.2.0) rexml (3.4.4) rouge (3.28.0) - rubyzip (2.4.1) - security (0.1.5) + rubyzip (3.7.0) + security (0.3.0) signet (0.22.0) addressable (~> 2.8) faraday (>= 0.17.5, < 3.a) @@ -230,9 +233,9 @@ GEM tty-spinner (0.9.3) tty-cursor (~> 0.7) uber (0.1.0) - unicode-display_width (3.2.0) - unicode-emoji (~> 4.1) - unicode-emoji (4.2.0) + unicode-display_width (3.3.0) + unicode-emoji (~> 4.3) + unicode-emoji (4.3.0) uri (1.1.1) word_wrap (1.0.0) xcodeproj (1.28.1) @@ -254,7 +257,7 @@ PLATFORMS ruby DEPENDENCIES - fastlane (= 2.239.0) + fastlane (= 2.240.1) CHECKSUMS CFPropertyList (3.0.8) sha256=2c99d0d980536d3d7ab252f7bd59ac8be50fbdd1ff487c98c949bb66bb114261 @@ -263,15 +266,16 @@ CHECKSUMS artifactory (3.0.17) sha256=3023d5c964c31674090d655a516f38ca75665c15084140c08b7f2841131af263 atomos (0.1.3) sha256=7d43b22f2454a36bace5532d30785b06de3711399cb1c6bf932573eda536789f aws-eventstream (1.4.0) sha256=116bf85c436200d1060811e6f5d2d40c88f65448f2125bc77ffce5121e6e183b - aws-partitions (1.1283.0) sha256=125ba9cb65eb7ec2b0a2041a47d204d403b3f6d9aa62bffa53a6b95b04b1f1de - aws-sdk-core (3.254.1) sha256=518089e32134c3478cd4ec63d07fb966546a45e9e4cbf5f80d2cf16d5699d29b - aws-sdk-kms (1.130.0) sha256=a2e83662ca31b77a2a19c9aa2f40a98165a67270c18c718fe1c70d0cbd7cd749 - aws-sdk-s3 (1.229.0) sha256=7dd11a111875b3505d2ec0a0a383a6aab6c1badb3d4e94e7fbb958a24451f596 + aws-partitions (1.1290.0) sha256=44613571eda66decddca594c10c9cee354ce819842b5724ca4fb5115e2f06811 + aws-sdk-core (3.257.0) sha256=a92ad19c7ff668cec6688ee3200ee562da80647a3bbeed9fda02e8b0ca6492c6 + aws-sdk-kms (1.132.0) sha256=094b0097bb3be9d5c1ac87e971ca7f5aebb746801dfb5f0f576480bf98dca69c + aws-sdk-s3 (1.232.1) sha256=124440f3a44fbc28cc3a2dec6eaeeea2a236bfc33772005010a19baa62654870 aws-sigv4 (1.12.1) sha256=6973ff95cb0fd0dc58ba26e90e9510a2219525d07620c8babeb70ef831826c00 babosa (1.0.4) sha256=18dea450f595462ed7cb80595abd76b2e535db8c91b350f6c4b3d73986c5bc99 base64 (0.3.0) sha256=27337aeabad6ffae05c265c450490628ef3ebd4b67be58257393227588f5a97b benchmark (0.5.0) sha256=465df122341aedcb81a2a24b4d3bd19b6c67c1530713fd533f3ff034e419236c - bigdecimal (4.1.2) sha256=53d217666027eab4280346fba98e7d5b66baaae1b9c3c1c0ffe89d48188a3fbd + bigdecimal (4.1.3) sha256=61ebe1e5e559bdc3cc6f2c0ee7f427321fc838f59611c294356eb04d6e21cf66 + cgi (0.5.2) sha256=61ca30298171190fd4fa0d8018e57ada456eae9b7a2b78526debf7f0a0e6f8bb claide (1.1.0) sha256=6d3c5c089dde904d96aa30e73306d0d4bd444b1accb9b3125ce14a3c0183f82e colored (1.2) sha256=9d82b47ac589ce7f6cab64b1f194a2009e9fd00c326a5357321f44afab2c1d2c colored2 (3.1.2) sha256=b13c2bd7eeae2cf7356a62501d398e72fde78780bd26aec6a979578293c28b4a @@ -279,26 +283,26 @@ CHECKSUMS csv (3.3.6) sha256=aba61e7e507a66f03d45cb1f3c4b6359861c3504038b422962875dce099e4456 declarative (0.0.20) sha256=8021dd6cb17ab2b61233c56903d3f5a259c5cf43c80ff332d447d395b17d9ff9 digest-crc (0.7.0) sha256=64adc23a26a241044cbe6732477ca1b3c281d79e2240bcff275a37a5a0d78c07 - domain_name (0.6.20260902) sha256=4fac7f8e0a77dc85ec02f6534fdeff38e143d434ab8a86528fe68ca5bc45a042 + domain_name (0.6.20260921) sha256=b335949f69fae603fed0ae88c4e09a51e9d8e3ceef4c73a6cfde44eb5c8dee6d dotenv (2.8.1) sha256=c5944793349ae03c432e1780a2ca929d60b88c7d14d52d630db0508c3a8a17d8 emoji_regex (3.2.3) sha256=ecd8be856b7691406c6bf3bb3a5e55d6ed683ffab98b4aa531bb90e1ddcc564b erb (6.0.7) sha256=c5ca6dc25b0ef974a44dc8f59fe847577122483b1968a38dec305c60bf91ee92 - excon (1.6.0) sha256=ebe2ab83c055ef8e117c62c91a3535b966a59a64868188219d49ab90cd83b65f - faraday (2.14.3) sha256=1882247e6766615c8220b4392bf1d27f6ebb63d8e28267587cef1fb0bf37f278 + excon (1.7.1) sha256=dd2d870eece1b5ce4e4f9efd938e67a69ccd8c003c3825b638b937e1082eaac2 + faraday (2.14.4) sha256=9bb4408c44621b0dbaaaa39e5dbfc4dbe58cc6751b6eb94d16ae9e1f2f2fab6e faraday-cookie_jar (0.0.8) sha256=0140605823f8cc63c7028fccee486aaed8e54835c360cffc1f7c8c07c4299dbb faraday-follow_redirects (0.5.0) sha256=5cde93c894b30943a5d2b93c2fe9284216a6b756f7af406a1e55f211d97d10ad faraday-multipart (1.2.0) sha256=7d89a949693714176f612323ca13746a2ded204031a6ba528adee788694ef757 faraday-net_http (3.4.4) sha256=0e78af151747ed1b00f33e25973b4bc220d7f16c00c39676817c8b12331eb588 faraday-retry (2.4.0) sha256=7b79c48fb7e56526faf247b12d94a680071ff40c9fda7cf1ec1549439ad11ebe fastimage (2.4.1) sha256=c64bebd46b6fd8943ab70c1e6e85ff728f970f2e48f92ecd249b6bc3a540ad20 - fastlane (2.239.0) sha256=6d99c0ea639f44fd4ebb53d442e5557e9a64159e6ca60ba1f38517cfe02c5689 + fastlane (2.240.1) sha256=cf792bc99ec6af6574ea416cda6530f519c8b4b3a46b1a3b52488ebf1696aa5c fastlane-sirp (1.1.0) sha256=10bc94f9682efd8e1badfb31452a76dd8981f1f3a33717c765fde6d75b54d847 gh_inspector (1.1.3) sha256=04cca7171b87164e053aa43147971d3b7f500fcb58177698886b48a9fc4a1939 - google-apis-androidpublisher_v3 (0.99.0) sha256=a0452fdd99cb7672cc95cac07429305f8aaee54c22e4875224cf675b1ab59729 + google-apis-androidpublisher_v3 (0.108.0) sha256=11a8b1885dce62ef28d8b7f16225d67e60c77b5baf16310a31d33fb1dc3c2d4e google-apis-core (1.2.5) sha256=e21562b5627a0fc6a0c3165e429c3afed0f6a628eaa514e83f7204dda1adff69 google-apis-iamcredentials_v1 (0.28.0) sha256=0a92ffe6cc39c569554af2a77a25dfc61519ed8bbb64ab04cffdd352dc5ef106 google-apis-playcustomapp_v1 (0.18.0) sha256=44b277b9dee4a59ac5e9d98be1485edc5e382d2f9d73c79ae8908a455786a254 - google-apis-storage_v1 (0.66.0) sha256=fdf6d65d3fc77e7046b3494333a818ece9e2afd763bb161e1a7a9e70cf198351 + google-apis-storage_v1 (0.67.0) sha256=13befc89bf03512783f535d214590f15993b0c56411155fd8090ca607e1b8d20 google-cloud-core (1.9.0) sha256=ab55409f51488e8deefb6edcc1ce4771dfb5da2fe7b3bc075709a030c2b682a4 google-cloud-env (2.3.1) sha256=0faac01eb27be78c2591d64433663b1a114f8f7af55a4f819755426cac9178e7 google-cloud-errors (1.7.0) sha256=6e682f42d89aae08689f36f28495de629d756da076bafff0003bac7f8042ebb3 @@ -307,15 +311,15 @@ CHECKSUMS googleauth (1.17.4) sha256=acc2cac2a6011048f149c922cbc6c9675719e165aa5000c7fa3876c480fe9ae3 highline (2.0.3) sha256=2ddd5c127d4692721486f91737307236fe005352d12a4202e26c48614f719479 http-cookie (1.1.6) sha256=ba4b82be64de61dc281243dac70e3c382c45142f20268ed9276a3670c93feaa9 - io-console (0.9.2) sha256=efa74f891dd03c0939a931dfc6e74c2813d904763d456ea9762b0525e748db08 + io-console (0.9.4) sha256=a403304f32928ee85e56d457b4612ca43f2703b561a3258844992bd24085fbb1 irb (1.18.0) sha256=de9454a0703a54704b9811a5ef31a60c86949fbf4013fcf244fabc7c775248e3 jmespath (1.6.2) sha256=238d774a58723d6c090494c8879b5e9918c19485f7e840f2c1c7532cf84ebcb1 json (2.21.2) sha256=1f1d3b7cf2b3ba1a69beca0bb6db13d5438b80bff3cd54cdaaa620b9b07c1c6a - jwt (3.2.0) sha256=5419b1fe37b1da0982bd07051f573a8b8789ab724c2aa7e785e4784a3ed217d7 + jwt (3.3.0) sha256=44cc34fbc341c148233cf66641522b4bce0dfb1ce55e8073f05c56f44f4ee546 logger (1.7.0) sha256=196edec7cc44b66cfb40f9755ce11b392f21f7967696af15d274dde7edff0203 mini_magick (4.13.2) sha256=71d6258e0e8a3d04a9a0a09784d5d857b403a198a51dd4f882510435eb95ddd9 mini_mime (1.1.5) sha256=8681b7e2e4215f2a159f9400b5816d85e9d8c6c6b491e96a12797e798f8bccef - multi_json (1.21.1) sha256=e6126a31808e3b4d19f483c775ceac34df190dffa62adfb63a165ee14ba68080 + multi_json (1.21.2) sha256=245531eaf54bdba57aba515ff08b4503505c5e09911daa89c5a561e2f85bea8e multipart-post (2.4.1) sha256=9872d03a8e552020ca096adadbf5e3cb1cd1cdd6acd3c161136b8a5737cdb4a8 mutex_m (0.3.0) sha256=cfcb04ac16b69c4813777022fdceda24e9f798e48092a2b817eb4c0a782b0751 nanaimo (0.4.0) sha256=faf069551bab17f15169c1f74a1c73c220657e71b6e900919897a10d991d0723 @@ -332,15 +336,15 @@ CHECKSUMS pstore (0.2.1) sha256=03904d0f2c66579e96d1e6704cdabc0c88df7ea8ed8782d9f3569f6f6c702c1a public_suffix (7.0.5) sha256=1a8bb08f1bbea19228d3bed6e5ed908d1cb4f7c2726d18bd9cadf60bc676f623 rake (13.4.2) sha256=cb825b2bd5f1f8e91ca37bddb4b9aaf345551b4731da62949be002fa89283701 - rbs (4.1.3) sha256=0c4474a9751cdc14364bfad0b3e53678323bbdc2c31683b0445932867dbab8c4 + rbs (4.2.0) sha256=51f7b886dcc05bc09e10b901daa6a81829f6adc03101d6ca9ea4aac6103e0674 rdoc (8.0.0) sha256=03bf8c08a9639658855a0cfd77c0abca8325c227693f7f33f82957811348c469 reline (0.7.0) sha256=5b012d8e55dbf9d450f12bde2cf7d15ff546ae80b3f8f3b30e570d431815583d - representable (3.2.0) sha256=cc29bf7eebc31653586849371a43ffe36c60b54b0a6365b5f7d95ec34d1ebace + representable (3.3.0) sha256=22beca7d07104d8dfe30667eb267756c764c8bdc6c0f1481922502ecc4f0d8cf retriable (4.2.0) sha256=90c3b257472a1c02f2a3dfa64dd35a420f7a624cef1a5981e20fdac5730f8cdc rexml (3.4.4) sha256=19e0a2c3425dfbf2d4fc1189747bdb2f849b6c5e74180401b15734bc97b5d142 rouge (3.28.0) sha256=0d6de482c7624000d92697772ab14e48dca35629f8ddf3f4b21c99183fd70e20 - rubyzip (2.4.1) sha256=8577c88edc1fde8935eb91064c5cb1aef9ad5494b940cf19c775ee833e075615 - security (0.1.5) sha256=3a977a0eca7706e804c96db0dd9619e0a94969fe3aac9680fcfc2bf9b8a833b7 + rubyzip (3.7.0) sha256=65c19294da75297a939006f3516deacc33185fbd721ff1954f7a231db6d3e121 + security (0.3.0) sha256=bc9d06718258a96c8552ce4044bfe69144491b10d3403d74c4753a03a720ecf0 signet (0.22.0) sha256=b76d495ccb07ad35dbc89f3e920665a9d8ed717141955034005d7843dcfe4780 simctl (1.6.10) sha256=b99077f4d13ad81eace9f86bf5ba4df1b0b893a4d1b368bd3ed59b5b27f9236b terminal-notifier (2.0.0) sha256=7a0d2b2212ab9835c07f4b2e22a94cff64149dba1eed203c04835f7991078cea @@ -351,8 +355,8 @@ CHECKSUMS tty-screen (0.8.2) sha256=c090652115beae764336c28802d633f204fb84da93c6a968aa5d8e319e819b50 tty-spinner (0.9.3) sha256=0e036f047b4ffb61f2aa45f5a770ec00b4d04130531558a94bfc5b192b570542 uber (0.1.0) sha256=5beeb407ff807b5db994f82fa9ee07cfceaa561dad8af20be880bc67eba935dc - unicode-display_width (3.2.0) sha256=0cdd96b5681a5949cdbc2c55e7b420facae74c4aaf9a9815eee1087cb1853c42 - unicode-emoji (4.2.0) sha256=519e69150f75652e40bf736106cfbc8f0f73aa3fb6a65afe62fefa7f80b0f80f + unicode-display_width (3.3.0) sha256=4b7aa66a4b11db50f6f7e98411215cd0dd10eaecb665d36eec5bcacb3fa0b613 + unicode-emoji (4.3.0) sha256=11c02fa73290378c066bb0562cd4c87d8e1b706fbbe0059ca12746a4244de8ce uri (1.1.1) sha256=379fa58d27ffb1387eaada68c749d1426738bd0f654d812fcc07e7568f5c57c6 word_wrap (1.0.0) sha256=f556d4224c812e371000f12a6ee8102e0daa724a314c3f246afaad76d82accc7 xcodeproj (1.28.1) sha256=6f12670f00739d9817ca27ac89d6ef01cc86050e22a0bc08a3131487e5b5cddc From 949ad843cde7b336b75d21f96281fe76dc2731a3 Mon Sep 17 00:00:00 2001 From: Erick Dronski Date: Tue, 6 Oct 2026 17:57:05 -0400 Subject: [PATCH 4/7] Read release-tooling fixtures as UTF-8 regardless of locale The offline release-tooling suite read the Fastfile, a workflow file and release notes with the process default encoding. Under a non-UTF-8 locale (LANG=C, as in many minimal shells) 24 of 41 tests errored with "invalid byte sequence in US-ASCII" because those files contain non-ASCII punctuation. Pass encoding: "UTF-8" explicitly, matching the description read that already did. The suite now passes 41/41 under both C and en_US.UTF-8. Co-Authored-By: Claude Opus 5.5 --- fastlane/spec/lore_release_tooling_test.rb | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/fastlane/spec/lore_release_tooling_test.rb b/fastlane/spec/lore_release_tooling_test.rb index c15e4ed..da31316 100644 --- a/fastlane/spec/lore_release_tooling_test.rb +++ b/fastlane/spec/lore_release_tooling_test.rb @@ -174,8 +174,8 @@ def test_submission_actions_require_screenshot_provenance_but_preflight_does_not end def test_screenshot_upload_targets_and_verifies_the_exact_reviewed_package - fastfile = File.read(File.expand_path("../Fastfile", __dir__)) - workflow = File.read(File.expand_path("../../.github/workflows/screenshots-upload.yml", __dir__)) + fastfile = File.read(File.expand_path("../Fastfile", __dir__), encoding: "UTF-8") + workflow = File.read(File.expand_path("../../.github/workflows/screenshots-upload.yml", __dir__), encoding: "UTF-8") upload = lane_body("screenshots_upload") assert_includes upload, "EXPECTED_VERSION" @@ -222,7 +222,7 @@ def test_swap_latest_waits_for_review_cancellation_before_selecting_build end def test_submission_helper_uses_ready_review_submission_and_item_verification - fastfile = File.read(File.expand_path("../Fastfile", __dir__)) + fastfile = File.read(File.expand_path("../Fastfile", __dir__), encoding: "UTF-8") assert_includes fastfile, "get_ready_review_submission" assert_includes fastfile, "wait_for_review_submission_item!" @@ -331,7 +331,7 @@ def test_app_and_widget_must_share_the_release_version_and_floor def test_checked_in_release_configuration_has_matching_targets_and_release_notes settings = LoreReleaseTooling.project_release_settings(File.expand_path("../../project.yml", __dir__)) version = LoreReleaseTooling.release_version!(settings.fetch(:version), project_version: settings.fetch(:version)) - notes = File.read(File.expand_path("../release_notes/#{version}.en-US.txt", __dir__)).strip + notes = File.read(File.expand_path("../release_notes/#{version}.en-US.txt", __dir__), encoding: "UTF-8").strip assert (1..4000).cover?(notes.length), "The project version needs nonempty App Store release notes of at most 4000 characters" description = File.read(File.expand_path("../release_notes/#{version}.description.en-US.txt", __dir__), encoding: "UTF-8") assert description.valid_encoding?, "The reviewed description must be UTF-8" @@ -620,7 +620,7 @@ def lane_body(name) end def all_lane_bodies - lines = File.readlines(File.expand_path("../Fastfile", __dir__)) + lines = File.readlines(File.expand_path("../Fastfile", __dir__), encoding: "UTF-8") starts = lines.each_index.select { |index| lines[index].match?(/^ lane :\w+\b/) } starts.to_h do |start| name = lines[start].match(/^ lane :(\w+)\b/)[1] From 5438a63af78489cb97023fa4deea702515324fed Mon Sep 17 00:00:00 2001 From: Erick Dronski Date: Tue, 6 Oct 2026 17:57:26 -0400 Subject: [PATCH 5/7] Ignore signing assets, env files and local tool state A full-history secret scan found nothing committed, but the ignore list only covered .p8 keys. Add the other Apple signing formats (.p12, .cer, CSRs, provisioning profiles), .env files (keeping .env.example trackable), xcresult bundles and local Ruby/Node/SwiftPM/Supabase CLI state so none of them can be committed by accident. No tracked file matches the new patterns. Co-Authored-By: Claude Opus 5.5 --- .gitignore | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/.gitignore b/.gitignore index 7ba9335..24f8bad 100644 --- a/.gitignore +++ b/.gitignore @@ -27,3 +27,24 @@ fastlane/*.p8 tools/narration/.venv/ tools/narration/models/ tools/narration/out/ + +# Signing material and local credentials. Release signing comes only from +# fastlane match and GitHub Actions secrets; none of these belong in the repo. +*.p12 +*.cer +*.certSigningRequest +*.mobileprovision +*.provisionprofile +.env +.env.* +!.env.example + +# Local tool state +*.xcresult +.build/ +.swiftpm/ +.bundle/ +vendor/bundle/ +node_modules/ +supabase/.temp/ +supabase/.branches/ From 5b2b659f1f47b2532633726864a69228330c13fa Mon Sep 17 00:00:00 2001 From: Erick Dronski Date: Tue, 6 Oct 2026 18:07:47 -0400 Subject: [PATCH 6/7] Correct stale statements in the release runbooks The README links CI-SETUP.md and TESTFLIGHT.md as the operator docs, so they need to match the workflows that exist today: - PR and main runs also compile Release and run the StoreKit journeys, not only the unit tests. - Build numbers follow the project.yml marketing version and floor, not marketing version 1.0. - The screenshot upload guard compares SOURCE_SHA with the release_source_sha input and requires ancestry; it does not refuse every existing set or require SOURCE_SHA to equal the checkout. - Drop references to one machine's toolchain and to absolute paths on a private laptop; point submission at the Preflight / Submit workflow. Co-Authored-By: Claude Opus 5.5 --- CI-SETUP.md | 6 ++++-- TESTFLIGHT.md | 28 ++++++++++++++-------------- 2 files changed, 18 insertions(+), 16 deletions(-) diff --git a/CI-SETUP.md b/CI-SETUP.md index 7044da9..74e2ae2 100644 --- a/CI-SETUP.md +++ b/CI-SETUP.md @@ -6,7 +6,8 @@ Expo/EAS is not part of this release lane. ## What publishes -- A push to `main` or a pull request runs the unit-test gate only. +- A push to `main` or a pull request runs validation only: unit tests, an + unsigned Release compile, and the StoreKit purchase journeys. - TestFlight upload requires an explicit **Run workflow** action with `upload_to_testflight` enabled. - The upload job tests the exact checkout before it archives, signs, and uploads. @@ -86,6 +87,7 @@ assets into a throwaway CI keychain, archives Release, and uploads the IPA. - **No signing profile:** verify both app identifiers exist and the API key has sufficient Apple access; Match must create/fetch profiles for both targets. - **Duplicate build number:** rerun the workflow. The lane queries the latest - build for marketing version `1.0` and increments it. + TestFlight build for the marketing version in `project.yml` and uses one + above it, never below the project's `CURRENT_PROJECT_VERSION` floor. - **Upload passed but build is absent:** allow Apple processing time, then inspect App Store Connect processing errors and the workflow build logs. diff --git a/TESTFLIGHT.md b/TESTFLIGHT.md index 2312cd4..ef7852c 100644 --- a/TESTFLIGHT.md +++ b/TESTFLIGHT.md @@ -5,8 +5,7 @@ **App Store Connect app ID:** `6788171860` GitHub Actions is the authoritative native build, test, signing, archive, and -TestFlight lane. This machine has Command Line Tools rather than the full iOS -SDK; local Xcode is not required to publish Lore. +TestFlight lane. Local Xcode is not required to publish Lore. ## Release contract @@ -15,7 +14,8 @@ The workflow at `.github/workflows/ios-testflight.yml` always: 1. Checks out one exact commit. 2. Selects Xcode 26.3 and generates `Lore.xcodeproj` with XcodeGen. 3. Runs the Lore unit-test suite on an iPhone simulator. -4. Compiles the unsigned Release configuration. +4. Compiles the unsigned Release configuration and runs the StoreKit purchase + journeys. 5. Archives, signs, and uploads only when a manual dispatch sets `upload_to_testflight=true`. @@ -78,20 +78,20 @@ Account deletion should use a disposable account. ## Screenshots are a separate release artifact -`fastlane screenshots_upload` intentionally cannot upload the existing -promotional set. The lane requires -`fastlane/promo_screenshots/SOURCE_SHA`, containing the full Git commit SHA from -which every screenshot was captured, and refuses to run unless it equals the -current checkout. +`fastlane screenshots_upload` (the **App Store · Upload Screenshots** workflow) +uploads `fastlane/promo_screenshots` only when +`fastlane/promo_screenshots/SOURCE_SHA` exactly matches the +`release_source_sha` dispatch input and that commit is an ancestor of the +checkout. It never touches the binary or text metadata. Regenerate the complete set from the exact release build, including a genuine physical-device scanner capture. Audit every visible claim, flatten alpha, and -verify App Store Connect's current dimensions before creating `SOURCE_SHA`. -Never upload the old map/profile set. +verify App Store Connect's current dimensions before recording `SOURCE_SHA` and +`CAPTURE.json`. ## App Review is separate -A processed TestFlight build is not approval to submit version 1.2 for review. -Use `/Users/dron/Projects/lore/legal/APP-STORE-LISTING.md` and `HANDOFF.md` for -the current submission gates. Explicit approval is required before pressing -Submit for Review. +A processed TestFlight build is not approval to submit a version for review. +Submission goes through the **App Store · Preflight / Submit** workflow and the +guards described in `docs/APP-STORE-RELEASE-TOOLING.md`. Explicit approval is +required before submitting for review. From c31cab24eb729da358009303e70a9e1688f59341 Mon Sep 17 00:00:00 2001 From: Erick Dronski Date: Tue, 6 Oct 2026 18:07:56 -0400 Subject: [PATCH 7/7] Rewrite the README around the shipped app and verified facts The old README undersold the project ("tested native build through a manual TestFlight lane") and carried claims that no longer matched the code. The rewrite is built from what the repository and public records show: - Status: version 1.2 is live on the App Store (iTunes lookup, released 2026-09-05); build 48 was uploaded by the manual TestFlight lane from 261d633. A dynamic shields.io badge reads the live store version. - Screenshots from the committed promo set, with provenance linked. - Architecture: app layers, the on-device geospatial pipeline (LocationHeadingProvider, BearingProjector, ScannerRanking, Vision, ARKit geo coverage), and the Supabase boundary (hand-written REST client, RLS-scoped user rows, Edge Functions, private photo bucket, quarantined functions directory). - Privacy: when-in-use location only, no coordinates sent to Lore's servers, visits store a place ID, one-frame opt-in image match with per-use confirmation, no tracking SDKs. - Build/test commands that were run locally, the 236-test suite, and a CI/release table taken from each workflow's triggers. - License stated plainly as source-available, all rights reserved. Removed: the claim that MapLibre is part of the shipped stack (it is linked behind a disabled flag), the statement that image matching is not a scanner control (it ships as an opt-in Lore+ action), and the Actions status badges, whose links 404 while Actions is disabled. Co-Authored-By: Claude Opus 5.5 --- README.md | 284 ++++++++++++++++++++++++++++++++++++++++-------------- 1 file changed, 212 insertions(+), 72 deletions(-) diff --git a/README.md b/README.md index fdf875a..ab55451 100644 --- a/README.md +++ b/README.md @@ -1,105 +1,245 @@ -# Lore for iPhone +# Lore for iPhone and iPad -

A native SwiftUI city guide for finding the stories hiding in the streets around you.

+[![App Store](https://img.shields.io/itunes/v/6788171860?label=App%20Store&logo=apple&logoColor=white&color=0D96F6)](https://apps.apple.com/us/app/lore-ar-city-history-guide/id6788171860) +![iOS 17+](https://img.shields.io/badge/iOS-17%2B-black) +![License: source-available](https://img.shields.io/badge/license-source--available%2C%20all%20rights%20reserved-lightgrey) -

- Product tour | - Architecture | - Build and test | - Security -

+Lore is a native SwiftUI city-history guide. Raise the camera on a street and +Lore ranks the catalog places around you by direction and distance, entirely on +the device, then opens a sourced story, timeline, gallery and narration for the +one you pick. Browse the map from home, follow self-guided walks, and keep a +private visit journal. -Lore is a curated city guide for iPhone. People can browse a living map, point the camera at chronicled places, read historical stories, follow walks, listen to narration, and keep a private visit journal. Coverage is curated city by city; Lore does not claim universal recognition or a source link for every catalog record. +**Status:** version 1.2 is [live on the App Store](https://apps.apple.com/us/app/lore-ar-city-history-guide/id6788171860) +(released September 5, 2026). Build 48 was archived, signed and uploaded to +TestFlight by this repository's manual release lane from commit +[`261d633`](https://github.com/erickdronski/lore-ios/commit/261d633d277a9940b849b108df0f10776e657f76). +`main` carries post-release fixes that have not shipped yet. -**Stack:** Swift, SwiftUI, MapKit, MapLibre, StoreKit 2, Supabase, XcodeGen, Fastlane, and GitHub Actions. +## Screenshots -## Product tour +Unedited simulator captures of the shipped 1.2 build. Provenance (source SHA, +device, Xcode and SHA-256 per frame) is recorded in +[`fastlane/promo_screenshots`](fastlane/promo_screenshots/README.md). - - - + + + - - - + + +
Lore city mapLore historical deep diveLore walking toursCity map with the Discovery Deck of nearby placesWillis Tower deep dive with story, gallery and narrationSelf-guided walking tours
Lore city cultureLore visit passportLore profile and progressMeet Chicago city culture guidePassport with visits and milestonesSigned-out profile
-## What ships - -- A city map, city switcher, search, place cards, deep dives, and culture content -- A live camera scanner that ranks known nearby places using on-device Apple frameworks and opens database-backed dossiers only after local geospatial confidence -- Curated walking tours and an on-device tour Live Activity -- Passport, visits, private notes and photos, and achievements -- Keychain-backed authentication and secure password recovery -- In-app account deletion and StoreKit 2 purchases and restoration - -Public contributions, background location, advertising, cross-app tracking, and unimplemented identity providers are deliberately excluded from the current release. +## Features + +- **Live scanner.** Camera, precise location, compass heading and device pitch + rank nearby catalog places, with an explicit confidence tier (locked pin, + bearing chip or directional hint) instead of a confident wrong answer. + On-device Vision adds scene categories and legible signage text. +- **Map and Discovery Deck.** MapKit city map, category filters, city switcher, + search, and an "Around you right now" rail of nearby places. +- **Place cards and deep dives.** Facts, stories, timelines, galleries, source + links, Street View and Look Around, plus studio narration with an on-device + voice fallback. +- **Walking tours.** Curated routes with per-stop stories, route notes and a + tour Live Activity on the Lock Screen and Dynamic Island. +- **Passport and journal.** Visits, private notes and photos, milestones and + shareable place cards. +- **Accounts and Lore+.** Email, Sign in with Apple, Google and Facebook + (OAuth with PKCE); sessions in the Keychain; in-app account deletion. StoreKit + 2 subscriptions and lifetime purchase with server-side transaction + verification and restore. Browsing never requires an account. +- **Universal app.** iPhone and iPad (sidebar layout, landscape), Dynamic Type + and VoiceOver support, offline-tolerant content cache. ## Architecture ```mermaid flowchart LR - UI["SwiftUI app and widget"] --> Domain["Places, tours, journal, and StoreKit domains"] - Scanner["On-device camera and sensor pipeline"] --> Resolver["Nearby-place resolver"] - Resolver --> Domain - Domain --> API["Supabase API boundary"] - API --> Data["Curated city data and private user records"] - Actions["GitHub Actions"] --> Tests["Unit tests and unsigned Release build"] - Tests --> Release["Manual signed TestFlight lane"] + subgraph Device["iPhone / iPad"] + UI["SwiftUI features
Map · Scanner · Place card · Tours · Passport · Profile"] + Stores["@Observable stores
auth, entitlements, visits"] + Scanner["Scanner pipeline
CoreLocation + heading + motion
BearingProjector → ScannerRanking
Vision (category + text)"] + API["LoreAPI
URLSession REST client"] + Cache["AtlasCache
stale-while-revalidate disk cache"] + Widget["LoreWidget extension
tour Live Activity"] + end + subgraph Supabase["Supabase (private backend repo)"] + REST["PostgREST tables + RPCs
row-level security"] + Auth["GoTrue auth"] + Fn["Edge Functions
sync-apple-purchase · delete-account
streetview · landmark-id"] + Storage["Storage
private journal-photos bucket"] + end + UI --> Stores --> API + UI --> Scanner + Scanner --> Stores + API --> Cache + API --> REST & Auth & Fn & Storage + UI -. ActivityKit .-> Widget ``` -Normal scanning stays on-device. Lore does not store continuous camera video or continuous location history. The landmark-ID Edge Function remains gated infrastructure, not a primary scanner control; any cloud-assisted path must stay authenticated, explicitly disclosed, one-frame only, and server quota enforced before it is re-exposed. - -## Repository map - -| Path | Purpose | -| --- | --- | -| `Sources/Lore` | iPhone application source | -| `Sources/LoreWidget` | Widget and Live Activity extension | -| `Sources/LoreTests` | Unit and contract tests | -| `Sources/LoreUITests` | UI verification and screenshot automation | -| `supabase/content` | Editorial seed payloads (not Edge Functions; do not deploy from this repo) | -| `fastlane` | Tests, signing, screenshots, preflight, and TestFlight delivery | -| `.github/workflows` | CI, screenshots, App Store preflight, and release automation | +### App layers + +| Layer | Path | Responsibility | +| --- | --- | --- | +| App shell | `Sources/Lore/App` | Entry point, routing, deep links (`lore://place/…`, `lore://tour/…`) | +| Features | `Sources/Lore/Features/*` | One folder per surface: `Map`, `Scanner`, `PlaceCard`, `Tours`, `Passport`, `Profile`, `Auth`, `Premium`, `Culture`, `Search`, `Share`, `Onboarding` | +| Models | `Sources/Lore/Models` | `Codable` value types mirroring the Supabase rows (`Place`, `Story`, `Dive`, `Tour`, `Visit`, `Entitlement`, …) | +| Networking | `Sources/Lore/Networking` | `LoreAPI` (hand-written PostgREST/Auth/Functions/Storage client), `AtlasCache`, `Config` | +| Design system | `Sources/Lore/DesignSystem` | Type scale, color, motion, reusable components | +| Shared | `Sources/Shared` | Types compiled into both the app and the widget extension | +| Widget | `Sources/LoreWidget` | WidgetKit/ActivityKit extension for the tour Live Activity | + +### Geospatial pipeline + +1. `LocationHeadingProvider` streams when-in-use location (10 m accuracy, 3 m + distance filter), true heading (magnetic as a fallback) and camera pitch + while the scanner is open, + and requests temporary full accuracy only for that screen. +2. `BearingProjector` is pure great-circle math: bearing and distance from the + device to each candidate place, and its angular offset from where the camera + points. +3. `ScannerRanking` scores candidates on proximity, prominence, gaze, novelty + and interest, then caps the confidence tier using horizontal and heading + accuracy and pitch, so a noisy fix produces a hint rather than a pin. + `DisambiguationStack` presents close calls as "one of these". +4. `VisionRecognitionService` classifies the frame and reads visible text on + the device. It never claims a specific landmark identity from pixels. +5. `GeoScoutingService` probes ARKit geo-tracking coverage before offering the + precise AR mode (`GeoARSessionController`). + +Place catalogs are fetched per city, not per coordinate, so ranking needs no +location on the server. The ranking and projection code has no I/O and is +covered by unit tests (`ScannerLogicTests`, `VisionRecognitionTests`). + +### Supabase boundary + +- The app talks to Supabase through `LoreAPI`, a dependency-free `URLSession` + client. Public catalog reads (`city`, `place_explore`, `story`, `dive`, + `tour`, `city_culture`, deal feeds, `search_lore` RPC) use the publishable + anon key; user rows (`visit`, `saved_place`, `user_prefs`, `entitlements`, + `user_achievement`) require the signed-in user's token and are scoped by + row-level security. +- The anon key in `Config.swift` is the publishable client key by design. No + service-role key exists anywhere in this repository or its history. +- Privileged work runs in Edge Functions: `sync-apple-purchase` re-verifies + StoreKit JWS transactions against Apple's chain before writing an + entitlement, `delete-account` performs fenced account deletion, `streetview` + keeps the Google Maps key server-side, and `landmark-id` serves the opt-in + image match. +- Schema migrations and Edge Function source live in the private `lore` + backend repository. `scripts/verify-edge-function-contracts.mjs` fails CI if + deployable function code ever appears under `supabase/functions` here. + `supabase/content` holds reviewed editorial seed payloads only. + +## Privacy and location data + +- **Location is when-in-use only.** There is no background location mode, no + continuous location history and no region monitoring. +- **Precise coordinates are not sent to Lore's servers.** The scanner, + Discovery Deck and tour guide use location on the device. Logging a visit + sends only the place ID and its source, so the server records which catalog + place a signed-in user marked, not where they were standing. +- **The live camera feed is never uploaded.** A frame leaves the device only + when the user sends it: the opt-in Lore+ "Identify landmark" action requires + sign-in, shows a disclosure and asks for confirmation before sending each + single still frame, and scanner postcards go only where the user shares them + through the system share sheet. Journal photos upload to a private storage + bucket and are served through signed URLs. +- **No tracking.** No ad or analytics SDKs, and `NSPrivacyTracking` is false. +- The [privacy manifest](Sources/Lore/PrivacyInfo.xcprivacy) declares what is + collected and why, and `PrivacyManifestTests` checks it against the shipping + data flows. The public policy is at + [lore-web-liart.vercel.app/privacy](https://lore-web-liart.vercel.app/privacy). ## Build and test -Requirements: macOS, Xcode, XcodeGen, Ruby, and Bundler. +Requirements: macOS with Xcode 26.3 or newer, [XcodeGen](https://github.com/yonaskolb/XcodeGen), +and Ruby 3.3+ with Bundler for the Fastlane lanes (CI uses Ruby 3.4). +`Lore.xcodeproj` is generated from `project.yml` and is not committed. ```sh +brew install xcodegen xcodegen generate -bundle install -bundle exec fastlane tests -``` -The generated Xcode project is not the source of truth and should not be edited manually. `.github/workflows/ios-testflight.yml` runs tests and an unsigned Release compile for source changes. TestFlight upload remains an explicit manual dispatch. +# Unit tests on a simulator (the scheme skips the StoreKit engine journeys) +xcodebuild test -project Lore.xcodeproj -scheme Lore \ + -destination 'platform=iOS Simulator,name=iPhone 17 Pro' -Local syntax and privacy-manifest checks: +# StoreKit 2 purchase, restore, refund and expiry journeys (local StoreKitTest +# engine; CI runs this job on Xcode 26.3, and StoreKitTest activation depends on +# the local Xcode and simulator pairing) +xcodebuild test -project Lore.xcodeproj -scheme LoreStoreKitTests \ + -destination 'platform=iOS Simulator,name=iPhone 17 Pro' \ + CODE_SIGNING_ALLOWED=YES CODE_SIGN_IDENTITY=- -```sh -swiftc -parse $(git diff --name-only -- '*.swift') -plutil -lint Sources/Lore/PrivacyInfo.xcprivacy \ - Sources/LoreWidget/PrivacyInfo.xcprivacy -``` - -## Release state - -- Marketing version: `1.2` -- Build: `48` -- Bundle ID: `com.erickdronski.lore` -- Minimum platform: iOS 17, universal iPhone and iPad -- Delivery: tested native build through a manual TestFlight lane - -The final release authority is a clean simulator test run plus a signed Release archive from the same commit uploaded to App Store Connect. +# The same unit-test gate CI runs, through Fastlane +bundle install +bundle exec fastlane tests -## Related repositories +# Offline tests for the release tooling (no Apple credentials needed) +ruby fastlane/spec/lore_release_tooling_test.rb +``` -App Store copy, legal pages, backend migrations, and public web/support routes live in separate private repositories. This repository is the public native shipping and TestFlight lane. +The unit suite has 236 tests across scanner math, Vision parsing, networking +resilience, StoreKit entitlement logic, account flows, privacy-manifest +contracts and layout. The app runs against the production Supabase project +with the publishable key; no local backend is needed. + +## CI and release pipeline + +| Workflow | Trigger | What it does | +| --- | --- | --- | +| [iOS CI and TestFlight](.github/workflows/ios-testflight.yml) | Pull request, push to `main`, manual | Unit tests, unsigned Release compile with version/device-family checks, StoreKit journeys. The signed TestFlight job runs only on a manual dispatch from `main` with `upload_to_testflight` set, after that run's test jobs pass. | +| [Release Tooling Tests](.github/workflows/release-tooling-tests.yml) | Pull request, push to `main` | Offline Minitest suite for the release guards in `fastlane/lib`. | +| [App Store · Preflight / Submit](.github/workflows/app-store-preflight.yml) | Manual only | Read-only submission audit, or one explicit, version-pinned App Store Connect action (prepare, select build, submit, hold, swap, auto-release). | +| [App Store · Screenshots](.github/workflows/screenshots.yml) | Manual only | Captures screenshots on the simulator with `fastlane snapshot`. No signing, no upload. | +| [App Store · Upload Screenshots](.github/workflows/screenshots-upload.yml) | Manual only | Uploads the committed screenshot set after proving it was captured from the named release commit. | + +Release design: + +- **Nothing ships from a push.** TestFlight uploads and every App Store + Connect action are manual workflow dispatches. The TestFlight job waits for + the same run's test jobs and refuses to run off `main`. App Store actions + take an explicit marketing version (and build, where one applies) instead of + "latest", and mutations fail closed unless the `APP_REVIEW_HOLD` variable + allows them. +- **Signing** uses Fastlane Match with encrypted certificates and profiles in a + private repository and an App Store Connect API key. Credentials live only in + GitHub Actions secrets ([CI-SETUP.md](CI-SETUP.md)). +- **Build numbers** are one above the latest TestFlight build and never below + the project floor, so reruns cannot collide. +- **Supply chain.** Every third-party action is pinned to a full commit SHA, + and Dependabot is configured for both the actions and the Bundler lockfile. + +The operator runbook is [TESTFLIGHT.md](TESTFLIGHT.md); the App Store guard +design is in [docs/APP-STORE-RELEASE-TOOLING.md](docs/APP-STORE-RELEASE-TOOLING.md). -## Contributing, security, and license +## Repository map -Read [CONTRIBUTING.md](CONTRIBUTING.md) before proposing a change. Report vulnerabilities privately using [SECURITY.md](SECURITY.md). Copyright 2026 Erick Dronski; see [LICENSE](LICENSE) for the source-available terms. +| Path | Purpose | +| --- | --- | +| `Sources/Lore` | Application source | +| `Sources/LoreWidget`, `Sources/Shared` | Widget/Live Activity extension and shared types | +| `Sources/LoreTests` | Unit, contract and StoreKit journey tests | +| `Sources/LoreUITests` | Screenshot capture and adaptive-layout UI checks | +| `StoreKit/Lore.storekit` | Local StoreKit configuration for simulator purchases and tests | +| `fastlane` | Test, screenshot, signing, TestFlight and App Store Connect lanes; release guards and their tests | +| `supabase/content` | Reviewed editorial seed payloads (no deployable functions) | +| `tools` | Content compilers, source-health checks, coverage audit and narration rendering | +| `docs` | Engineering standards, release tooling notes and working logs | +| `project.yml` | XcodeGen spec: targets, schemes, Info.plist keys, versions | + +## Contributing, security and license + +Read [CONTRIBUTING.md](CONTRIBUTING.md) before opening a pull request, and +report vulnerabilities privately through [SECURITY.md](SECURITY.md). + +The source is published for evaluation and review, not as open source. +Copyright 2026 Erick Dronski, all rights reserved: no permission is granted to +copy, modify or redistribute it without written consent. See [LICENSE](LICENSE).