🚀 [Release v3.9.17] - #191
Closed
grunt-claude-bot wants to merge 216 commits into
Closed
Conversation
Compiled!!!
…elop # Conflicts: # .gitignore # LICENSE
- Removed nothing test
update references to strings - Readded BIP39 list update rough localization strings Updated the core library move references to critical files
- removed cruft - updated dark light mode for logo - removed extra image
* Fix CircleCI build failure: init and build the bw-gdlib xcframework build_1841 (run_unit_tests_iPhone16ProMax) failed with: error: There is no XCFramework found at '/Users/distiller/ios/Private/bw-gdlib/ios/build/robovm/BWIOSGdx.xcframework'. (in target 'brainwallet' from project 'brainwallet') Two things were missing: 1. The submodule init step only initialized Modules/core and Private/general-purpose -- Private/bw-gdlib was never checked out, so the directory didn't exist at all on the CI box. 2. Even with the submodule checked out, BWIOSGdx.xcframework still wouldn't exist: bw-gdlib's ios/build/ is gitignored (same as any other Gradle build output), because it's a RoboVM AOT-compiled artifact, not something checked into git. It has only ever been built manually on developers' Macs -- neither this repo's CI nor bw-gdlib's own CircleCI config (which runs on Linux and only does ./gradlew :core:test) has ever produced it. Fix: init Private/bw-gdlib alongside the other submodules, and add a build step that runs `./gradlew :ios:robovmInstall` (bw-gdlib's Gradle task that produces build/robovm/BWIOSGdx.xcframework, confirmed via `./gradlew :ios:tasks --all`) before the fastlane test run, installing a JDK first if the macOS executor doesn't already have one. Not yet verified against a live CircleCI run (no local runner available) -- please confirm the next push on this branch turns the job green, particularly the RoboVM build step's timing and the openjdk@17 Homebrew install path on the macos.m1.medium.gen1 executor. Co-Authored-By: grunt-claude-bot <308083480+grunt-claude-bot@users.noreply.github.com> Co-Authored-By: kcw-grunt <mrkerrywashington@icloud.com> * Avoid needing BWIOSGdx.xcframework in CI at all The previous commit tried to fix CI by building the framework there (git submodule init + `./gradlew :ios:robovmInstall`), but that failed too: bw-gdlib has its own nested submodule (android-build-logic, for a Gradle version catalog) that a non-recursive `submodule update --init Private/bw-gdlib` doesn't fetch, so Gradle failed on a missing libs.versions.toml. Chasing that down further, building the framework in CI was solving the wrong problem. Simulator builds never need BWIOSGdx at all -- the game is entirely #if !targetEnvironment(simulator)-guarded in Swift -- but Xcode's Frameworks and Embed Frameworks build phases still referenced the xcframework unconditionally for every platform, so Xcode tried to resolve/embed the file regardless and errored with "There is no XCFramework found" the moment it didn't exist, before our existing simulator-only linker scoping ([sdk=iphoneos*]) ever mattered. Fix: - Remove BWIOSGdx.xcframework from the Embed Frameworks build phase too (it was already removed from the Frameworks/link phase in the simulator-crash fix). Xcode now never touches the file for any platform. - Move the embedding Xcode used to do automatically into the "Flatten+Sign+Relocate Asset Frameworks" script instead, gated the same way as the rest of that script: skip entirely on Simulator, copy+sign+flatten it for device builds (erroring with a clear message if the file's missing there, instead of Xcode's opaque one). - Revert the CircleCI submodule-init and Gradle build step from the previous commit -- no longer needed since Simulator/CI builds don't touch BWIOSGdx at all now. Verified locally: moved Private/bw-gdlib/ios/build/robovm/BWIOSGdx.xcframework out of the way entirely (reproducing a fresh CI checkout) and did a clean Simulator build -- BUILD SUCCEEDED, app installs and launches normally. Restored the framework afterward and confirmed device-slice FRAMEWORK_SEARCH_PATHS/embedding logic is unchanged for real device builds. Co-Authored-By: grunt-claude-bot <308083480+grunt-claude-bot@users.noreply.github.com> Co-Authored-By: kcw-grunt <mrkerrywashington@icloud.com> * Fix Xcode Cloud ci_post_clone.sh: actually write the secret files ci_post_clone.sh wrote the literal strings "GOOGLE_SERVICES_PLIST", "REMOTE_CONFIG_DEFAULTS", and "DEBUG_SERVICE_DATA" into their respective files -- missing the '$' to dereference the environment variables entirely, so GoogleService-Info.plist etc. never contained real content on any Xcode Cloud run. Even with '$' added it would still be wrong: these vars are base64-encoded in Xcode Cloud's environment variable settings, same as their proven-working CircleCI counterparts in .circleci/config.yml's "Setup environment files" step, and this script never decoded them. Also: Brainwallet-StoreKit-v1.storekit was never written for Xcode Cloud at all, unlike CircleCI which creates it from BRAINWALLET_IOS_STOREKIT_V1_0_FILE alongside the other three files. Fix: dereference + base64-decode all four env vars into PreLaunchResources, matching CircleCI's approach, and fail fast with a clear message if GoogleService-Info.plist doesn't decode to a valid plist (via PlistBuddy) instead of silently shipping garbage. Verified via a dry run with real base64-encoded fake values: decoded content matches exactly, and the PlistBuddy validation passes. Not yet verified against a live Xcode Cloud run -- no CI report was provided for this one, this was found via code review (comparing against CircleCI's already-proven equivalent step). If Xcode Cloud also runs an Archive/TestFlight workflow (as opposed to only the brainwalletUITests scheme this script pre-resolves packages for), it will still need Private/bw-gdlib's BWIOSGdx.xcframework built via the existing "Build RoboVM" Xcode build phase (runOnlyForDeploymentPostprocessing, so archive-only) -- worth checking submodule access is granted to bw-gdlib in Xcode Cloud's source control settings if that workflow exists and fails. Co-Authored-By: grunt-claude-bot <308083480+grunt-claude-bot@users.noreply.github.com> Co-Authored-By: kcw-grunt <mrkerrywashington@icloud.com> * Fail fast on bad CI secrets instead of a cryptic xcodebuild plist error Xcode Cloud build 431 (brainwalletUnitTests build-for-testing) failed 90+ seconds into the actual xcodebuild step with: error: unable to read input file as a property list: The operation couldn't be completed. (SWBUtil.PropertyListConversionError error 2.) (in target 'brainwallet' from project 'brainwallet') during the CopyPlistFile resource-processing step for service-data.plist. Traced it to the root cause: DEBUG_SERVICE_DATA's value in Xcode Cloud's environment variable settings (App Store Connect) doesn't base64-decode to a valid plist. (ci_post_clone.sh's base64 decoding itself is working correctly -- confirmed from this same build's ci_post_clone.log: GoogleService-Info.plist and remote-config-defaults.plist, decoded by the identical code path, copied without error.) That's a secret misconfigured on Apple's side, outside this repo -- flagging for whoever manages Xcode Cloud's App Store Connect settings to fix. What this commit does fix: neither CI script actually verified the other three generated files were valid before handing them to xcodebuild, so a bad secret's failure mode was a confusing build-system error far removed from its actual cause. Extended validation (already added for GoogleService-Info.plist after the ci_post_clone.sh env-var bug) to remote-config-defaults.plist and service-data.plist (PlistBuddy) and Brainwallet-StoreKit-v1.storekit (python3 -m json.tool -- it's JSON, not a plist, so plutil -lint rejects it outright regardless of validity). Applied to both ci_scripts/ci_post_clone.sh and .circleci/config.yml's "Setup environment files" step, since both decode the same four env vars and either could hit the same class of bug. Verified via dry runs with real base64-encoded fixtures: all 4 valid -> clean pass; DEBUG_SERVICE_DATA broken (matching build 431's actual failure) -> fails immediately with the exact var/file named, instead of xcodebuild's opaque error later. Co-Authored-By: grunt-claude-bot <308083480+grunt-claude-bot@users.noreply.github.com> Co-Authored-By: kcw-grunt <mrkerrywashington@icloud.com> * Fix locale-dependent test failure in testISO8601DateFormatterFormat Surfaced on Xcode Cloud build #436, the first build to actually get brainwalletUnitTests compiling and running there: [TEST_FAILURE] NewReceiveTests.swift:192: XCTAssertTrue failed The test asserted the formatted date string contains the English month abbreviation "jun", but configured the formatter with Locale.current -- Xcode Cloud's build agent's default locale doesn't format months the same way as a developer's en_US Mac, so the assertion failed for a reason that has nothing to do with the date formatting logic under test. Fixed by using a deterministic en_US_POSIX locale, standard practice for locale-independent date-format tests. Verified locally: testISO8601DateFormatterFormat passed (0.413s) via xcodebuild test-without-building -only-testing. Co-Authored-By: grunt-claude-bot <308083480+grunt-claude-bot@users.noreply.github.com> Co-Authored-By: kcw-grunt <mrkerrywashington@icloud.com> * Pin timeZone too -- the locale fix alone wasn't enough Xcode Cloud build #438 (on af4a2f4, the locale fix) still failed the exact same assertion: [TEST_FAILURE] NewReceiveTests.swift:197: XCTAssertTrue failed XCTAssertTrue(formattedString.contains("jun")) en_US_POSIX fixed the locale half of the problem but not the other half: formatter.timeZone was never pinned, so it inherited the executing machine's system time zone. The test date (1717200000) is June 1 00:00:00 UTC, which is still May 31 in any zone west of UTC: TZ=America/Los_Angeles date -r 1717200000 -> Fri May 31 17:00:00 PDT 2024 TZ=America/New_York date -r 1717200000 -> Fri May 31 20:00:00 EDT 2024 My Mac happens to be on BST (UTC+1), where that instant is still "01 Jun" -- which is exactly why this passed locally both times but kept failing on Xcode Cloud's build agent, whose system time zone is apparently west of UTC. Pinned formatter.timeZone to UTC, matching what the test's own comment already claims the date represents. Verified locally: testISO8601DateFormatterFormat passed (0.083s). Co-Authored-By: grunt-claude-bot <308083480+grunt-claude-bot@users.noreply.github.com> Co-Authored-By: kcw-grunt <mrkerrywashington@icloud.com> * Stop printing decoded GoogleService-Info.plist content to CI logs Flagged directly: does anything in this branch leak secrets, given the repo is public? ci_post_clone.sh itself is clean -- confirmed it only ever references env var *names*, never literal values, the four generated files are all gitignored, and none are currently tracked. But .circleci/config.yml had a pre-existing (not introduced by this branch) `head -10 GoogleService-Info.plist` right after decoding it, intended as a "safe" debug dump ("avoid showing sensitive data" per its own comment) but not actually safe: this repo is public, and CircleCI serves build logs for public GitHub repos without requiring a CircleCI login by default, so the first 10 lines of a small XML plist (API_KEY, GOOGLE_APP_ID, etc. included) were one real CircleCI run away from being publicly readable. Replaced with a comment pointing at check_plist (added earlier in this branch), which already validates the decoded content is a real plist via PlistBuddy without ever printing it -- the correct way to verify this. Co-Authored-By: grunt-claude-bot <308083480+grunt-claude-bot@users.noreply.github.com> Co-Authored-By: kcw-grunt <mrkerrywashington@icloud.com> * Fix the same GoogleService-Info.plist log-leak in the other 2 CircleCI configs 0a383bf fixed `head -10 GoogleService-Info.plist` in .circleci/config.yml (printing decoded secret content into build logs of a public repo, whose CircleCI logs are viewable without a login by default). Asked to check the rest of the repo for similar leaks: the exact same block was copy-pasted into build-uitest-config.yml and daily-develop-config.yml too, unfixed. Applied the identical fix to both: content-blind plist validation via PlistBuddy (check_plist) instead of printing decoded lines. Also added the StoreKit JSON validation to daily-develop-config.yml, which writes that file but never validated it (build-uitest-config.yml doesn't decode a StoreKit file at all, so it doesn't need that check). Everything else checked clean: no .p8/.storekit/provisioning profiles/real GoogleService-Info.plist ever committed anywhere in this repo's current tree; GitHub Actions workflows use native secrets.X (auto-redacted by GitHub); Fastfile doesn't touch any of these files. Separately (not fixed here, flagged to the user directly): a real service-data.plist with actual wallet-ops/AppsFlyer data was committed in this repo's early history (033e2b4, 6575eb1) and is still reachable from origin/develop and ~100 branches -- a git-history exposure, not a CI-log one, and a materially different fix (rotation + possible history rewrite) that needs the user's explicit direction before anyone touches it. Co-Authored-By: grunt-claude-bot <308083480+grunt-claude-bot@users.noreply.github.com> Co-Authored-By: kcw-grunt <mrkerrywashington@icloud.com> --------- Co-authored-by: kcw-grunt <mrkerrywashington@icloud.com> Co-authored-by: grunt-claude-bot <308083480+grunt-claude-bot@users.noreply.github.com>
) Pulls in core's fix for Firebase Crashlytics issue 9f86d0dd9962b0efc34252c4c41b2659 (gruntsoftware/core#19): EXC_BAD_ACCESS at address 0x0 in _peerThreadRoutine. pthread_cleanup_pop(1) invoked ctx->threadCleanup unconditionally, unlike every other optional callback in the same function, which are null-checked first -- if threadCleanup was ever unexpectedly NULL, that's a call through a null function pointer, exactly matching this crash's signature. Now guarded the same way, falling back to the existing no-op default. Matches the ef1bda5 precedent: root-cause and fix land in gruntsoftware/core via its own fix/crashlytics-<hash> branch and PR (gruntsoftware/core#19, merged, verified there with `make test` -- zero new failures vs. unpatched main), this commit just bumps the pinned submodule commit to pull it in. Co-authored-by: kcw-grunt <mrkerrywashington@icloud.com> Co-authored-by: grunt-claude-bot <308083480+grunt-claude-bot@users.noreply.github.com>
… BWIOSGdx link (#156) * Update project * update game to v1.6.7(8) * update the v1.6.8 * Fix Simulator crash: remove BWIOSGdx.xcframework re-added to Frameworks/Embed 014f068 ("update the v1.6.8") re-added BWIOSGdx.xcframework to the unconditional Frameworks and Embed Frameworks build phases -- almost certainly via Xcode's own "add framework" UI while troubleshooting a device build, since that's Xcode's default behavior for any framework added through its interface. This undoes ios#154's device-only-scoped linking fix (OTHER_LDFLAGS/ FRAMEWORK_SEARCH_PATHS[sdk=iphoneos*], deliberately NOT using the Frameworks/Embed Frameworks phases, since phase membership doesn't respect SDK qualifiers and applies to every platform unconditionally). Harmless on device (just a redundant second link path alongside the scoped one), but reintroduces the exact Simulator crash that fix eliminated: Simulator tries to link/embed BWIOSGdx.xcframework, whose simulator slice has device-only vendored native libs (gdx-box2d/gdx-freetype/gdx-miniaudio/gdx core -- byte-identical to the device slice, confirmed earlier this session). Verified: reproduced against a clean `develop` in an isolated worktree first (fresh clone, fresh submodules, no local changes) to confirm this wasn't actually a develop regression -- builds, installs, and launches fine there, proving the bug was local-only. Then applied the same fix here: BUILD SUCCEEDED, installs, launches, and stays running on Simulator. Co-Authored-By: grunt-claude-bot <308083480+grunt-claude-bot@users.noreply.github.com> Co-Authored-By: kcw-grunt <mrkerrywashington@icloud.com> * bumped --------- Co-authored-by: kcw-grunt <mrkerrywashington@icloud.com> Co-authored-by: grunt-claude-bot <308083480+grunt-claude-bot@users.noreply.github.com>
Removes code and resources confirmed unused across the ios repo (submodules excluded), validated with a full clean build + test build after every batch: - Whole orphaned directories not referenced by the Xcode project: brainwallet/Launch Resources/ (stale duplicate of PreLaunchResources/) and brainwalletTests/ (leftover from a retired test target). - 55 orphaned .swift files never wired into any build target, mostly stale duplicates left behind by the SwiftUI rewrite (e.g. root-level ApplicationController.swift, BuyViewModel.swift, TabBarViewController.swift), plus a fully commented-out UtilityTests.swift. - Unused code inside otherwise-live files: dead Redux actions in Actions.swift (and their now-orphaned clone helpers), an unused GradientView/GradientDrawable gradient-drawing path, PaymentProtocolACK/PaymentProtocolPayment, several unused SwiftUI view modifiers/structs, and a few commented-out dead-code blocks. - Dead resources: 3 unused product images (amazon_logo, je_logo, visa_logo), 3 sound files bundled but never played (clicksound.wav, clicksound.aiff, clickseedword.mp3), and a stray committed testScript Mach-O binary. project.pbxproj updated to match everywhere a compiled file or resource reference was removed. Co-authored-by: kcw-grunt <mrkerrywashington@icloud.com> Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
Checkpoint build covering everything merged into develop over the last 48 hours (2026-08-13 through 2026-08-14): - #153 Fix Simulator launch crash + flaky LockScreenViewUITests: link BWIOSGdx.xcframework device-only (via OTHER_LDFLAGS/ FRAMEWORK_SEARCH_PATHS[sdk=iphoneos*] instead of the unconditional Frameworks build phase) so Simulator builds never require its device-only vendored natives; also stabilized a flaky lock-screen UI test that depended on locale/theme-dependent SF Symbol labels. - #154 Fix CircleCI + Xcode Cloud build failures: stopped trying to build BWIOSGdx.xcframework in CI (Simulator never needs it) and moved its embedding into the existing Flatten+Sign+Relocate script; fixed ci_post_clone.sh to actually dereference and base64-decode its secret env vars into GoogleService-Info.plist / remote-config-defaults.plist / service-data.plist / Brainwallet-StoreKit-v1.storekit; added fail-fast validation for all four generated files across both CI providers; fixed a locale- and timezone-dependent date-formatter test failure; and removed a GoogleService-Info.plist content dump from three CircleCI configs that could have leaked API keys in this public repo's logs. - #155 Bump Modules/core: pulls in a fix for a Crashlytics-reported EXC_BAD_ACCESS in _peerThreadRoutine (null-checks threadCleanup before invoking it, matching every other optional callback in the same function). - #156 Bump bw-gdlib to v1.6.8, fix Simulator crash regression: the game-SDK bump's Xcode-UI framework changes re-added BWIOSGdx.xcframework to the unconditional Frameworks/Embed phases, regressing #153's Simulator fix; removed it from those phases again. - #157 chore: remove defunct code, resources, and dead files: 328 files / ~12.5k lines removed across the ios repo (orphaned Launch Resources/ and brainwalletTests/ directories, 55 files never wired into any build target, dead code cascades inside otherwise-live files, and unused images/sounds), validated with a full clean build and test-build after every batch. This commit itself: build-number bump only, plus Xcode's own formatting pass over Localizable.xcstrings (key/value spacing) picked up incidentally when the project was last opened. Co-authored-by: kcw-grunt <mrkerrywashington@icloud.com> Co-authored-by: grunt-claude-bot <308083480+grunt-claude-bot@users.noreply.github.com>
…o-98%-sync (#161) * fix: sync-duration analytics event never fired; log foreground time-to-98%-sync user_did_complete_sync required walletState.syncProgress >= 0.999 AND walletState.syncState == .syncing in the same dispatched state, but WalletCoordinator.onSyncStop() invalidates the progress timer and dispatches setSyncingState(.success) as a separate store action from the final setProgress. By the time the last block-height update landed, syncState had usually already flipped to .success, so the isSyncing gate skipped the check entirely -- the event essentially never fired. Goal is a foreground-sync-duration average, to be able to promote the shortest sync time to users -- so this measures accumulated active syncing time while the app was in the foreground, not wall-clock time from wallet creation to completion (which would include however long the app sat backgrounded/closed mid-sync, and isn't a fair "how fast is our sync" number). - WalletCoordinator now owns the metric, since it's the actual owner of the sync start/stop lifecycle (not a Redux subscription racing against it). Tracks active-foreground-sync segments: a segment opens on onSyncStart or didBecomeActiveNotification (only if still mid-sync), and closes on onSyncStop or willResignActiveNotification -- so the ~30s grace period the peer manager can keep running into the background (until the background task's expiration handler disconnects it) isn't counted as foreground time either. Elapsed segments accumulate into UserDefaults.foregroundSyncDurationSeconds. Once block-height progress first crosses kSyncDurationThreshold (98%), logs the accumulated total in seconds as a Firebase parameter: user_did_complete_sync now sends sync_duration_seconds instead of parameters: nil. - Both foregroundSyncDurationSeconds and the one-shot hasLoggedInitialSyncDuration flag are persisted (a sync can span multiple app sessions before reaching 98%), and both get wiped for free by the existing wipeWallet() (removePersistentDomain), so a fresh wallet gets a fresh measurement window with no extra cleanup. - Outlier cap: durations past kMaxSyncDurationSeconds (24h) are discarded rather than logged. Matters less now that background time no longer leaks into the number in the first place, but kept as a backstop against clock skew / a pathologically bad connection. - NewMainViewModel.didRestoreOldBrainwallet(): dispatch WalletChange.setWalletCreationDate(Date()), matching generateNewWallet() -- Restore never set this, so walletState.creationDate (persisted into WalletInfo via KVStoreCoordinator) stayed at Date.zeroValue() for every restored wallet. Independent fix, kept even though the duration metric no longer depends on creationDate. Verified via xcodebuild build and build-for-testing (both succeed). Still needed: register sync_duration_seconds as a Custom Definition in the Firebase console (Analytics > Custom definitions) to use it in Reporting -- that's a console-side step, not code. p50/p90 aren't something the client can compute (each wallet only ever sends one value); they require the BigQuery export linked and a percentile query over sync_duration_seconds, not a code change. Co-Authored-By: grunt-claude-bot <308083480+grunt-claude-bot@users.noreply.github.com> Co-Authored-By: kcw-grunt <mrkerrywashington@icloud.com> * fix: FallinScene/WelcomeFallinScene leak forever via unweakified recursive delay (#162) FallinScene.makeDot() reschedules itself via delay(2.0) { ... self.makeDot() ... } with no [weak self], no cancellation, and no termination condition. Every call -- including the automatic one from didMove(to:) -- starts a GCD timer chain on the main queue that fires every ~2s for the rest of the process's life; the closure's strong self-capture means the scene can never be deallocated once it starts. This isn't test-only: FallinScene is live via FallinMojiDemoView -> GameHubBentoView, so every time a user opens the Game Hub bento, this leaks a scene that recurses forever in the background. Root-caused BrainwalletUnitTests failure: FallinSceneTests.testMakeDot_CreatesLabelNode() Asynchronous wait failed: Exceeded timeout of 1 seconds, with unfulfilled expectations: "Wait for node creation" scene.makeDot() itself is synchronous (addChild happens immediately); the wait is just scaffolding. The 6+ tests in FallinSceneTests each call makeDot(), leaving behind permanently-recursing orphaned scenes that pile up competing for the main run loop across the suite -- enough backlog to make a trivial 0.1s asyncAfter miss a 1s timeout. Fix: [weak self] + guard in the recursive closure, so an orphaned scene can actually be deallocated once nothing else references it, stopping the chain. WelcomeFallinScene.makeSprites() has the same pattern (delay(1.0) { ... self.makeSprites() ... }, no [weak self]). It does self-terminate (the 30s countdown reaching 0 breaks the chain), but without weakifying, a user backing out of the view mid-game keeps it recursing and mutating @binding var counter/countdown/didStartGame for up to 30s after the view is gone. Fixed the same way, plus the same missing [weak self] in touchesBegan's one-shot delay(0.1) (smaller window, same pattern). Verified: ran FallinSceneTests directly on iPhone 16 Pro (iOS 18.5) -- all 8 tests pass, including testMakeDot_CreatesLabelNode() at 0.107s. xcodebuild build and build-for-testing both succeed. Co-authored-by: kcw-grunt <mrkerrywashington@icloud.com> Co-authored-by: grunt-claude-bot <308083480+grunt-claude-bot@users.noreply.github.com> --------- Co-authored-by: kcw-grunt <mrkerrywashington@icloud.com> Co-authored-by: grunt-claude-bot <308083480+grunt-claude-bot@users.noreply.github.com>
Bumps Private/bw-gdlib from ab90257 to 68b03cb: - Add QR privacy hide/show toggle to GameEndView - Bump fallinmoji version to 1.6.9 (code 10) - Update README release notes and badge for v1.6.9 Co-authored-by: kcw-grunt <mrkerrywashington@icloud.com> Co-authored-by: grunt-claude-bot <noreply@anthropic.com> Co-authored-by: kcw-grunt <kerry@grunt.ltd>
* Update project.pbxproj * chore: bump build to 2602307 Co-Authored-By: grunt-claude-bot <308083480+grunt-claude-bot@users.noreply.github.com> Co-Authored-By: kcw-grunt <mrkerrywashington@icloud.com> --------- Co-authored-by: kcw-grunt <mrkerrywashington@icloud.com> Co-authored-by: grunt-claude-bot <308083480+grunt-claude-bot@users.noreply.github.com>
* chore: remove CircleCI, rely on Xcode Cloud for CI CircleCI's macOS executor costs had gotten too high. Xcode Cloud (ci_scripts/) already runs the build/test pipeline, so it's now the sole CI for this repo. - Delete .circleci/ (per-push unit tests, daily-develop maintenance job, and UI-test/weekly-maintenance job) - Remove setup_circle_ci from fastlane/Fastfile's before_all - Move the Linux-only i18n translation-coverage check (scripts/test_i18n_coverage.py) to a new GitHub Actions workflow (.github/workflows/i18n-coverage.yml) since it has no Xcode Cloud equivalent - Drop the CircleCI status badge and the CircleCI-updated Gist tests-badge from README; update the Testing section to reference Xcode Cloud + the new i18n workflow - Clean up stale .circleci references in ci_scripts/ci_post_clone.sh and .github/workflows/pr-summary.yml Co-Authored-By: grunt-claude-bot <308083480+grunt-claude-bot@users.noreply.github.com> Co-Authored-By: kcw-grunt <mrkerrywashington@icloud.com> * remove duplicate bwiosgdx * bumped code --------- Co-authored-by: kcw-grunt <mrkerrywashington@icloud.com> Co-authored-by: grunt-claude-bot <308083480+grunt-claude-bot@users.noreply.github.com>
* fix: clear easy Swift compiler warnings
Mechanical, behavior-preserving cleanup of the low-risk warnings from a
full warnings review: unused GeometryReader width/height locals, a dead
if-let block, unnecessary try? on a non-throwing call, a non-exhaustive
switch on UIGestureRecognizer.State, deprecated Data(bytes:) inits, a
write-only dead variable, and refactoring WalletCoordinator's manual
weak-var-capture pattern to the idiomatic [weak self] closure capture.
Co-Authored-By: grunt-claude-bot <308083480+grunt-claude-bot@users.noreply.github.com>
Co-Authored-By: kcw-grunt <mrkerrywashington@icloud.com>
* fix: replace deprecated UIApplication.windows with currentKeyWindow helper
UIApplication.windows was deprecated in iOS 15 in favor of going through
UIWindowScene. Adds a single UIApplication.currentKeyWindow extension
(via UIWindowScene.keyWindow across connectedScenes) and swaps in the
three call sites that were each independently doing the old
`.windows.filter { $0.isKeyWindow }.first` dance -- ModalPresenter.swift,
ModalPresenter+Extension.swift, and ModalTransitionDelegate.swift.
Co-Authored-By: grunt-claude-bot <308083480+grunt-claude-bot@users.noreply.github.com>
Co-Authored-By: kcw-grunt <mrkerrywashington@icloud.com>
* fix: replace deprecated SwiftUI .animation(_:) with value-scoped variant
.animation(_:) (no value:) implicitly animates any state change to the
view, which is why it was deprecated in iOS 15. Scope it to the specific
binding actually driving the change (shouldExpandBlockchain) instead.
Co-Authored-By: grunt-claude-bot <308083480+grunt-claude-bot@users.noreply.github.com>
Co-Authored-By: kcw-grunt <mrkerrywashington@icloud.com>
* fix: migrate SCNetworkReachability to NWPathMonitor
SCNetworkReachabilityCreateWithName/SetCallback/SetDispatchQueue/GetFlags
were deprecated in iOS 17.4. Rewrites ReachabilityMonitor on top of
Network.framework's NWPathMonitor, keeping its existing public surface
(didChange closure, isReachable) so its three call sites (WalletCoordinator,
ModalPresenter, ApplicationController) are unaffected. Also drops the
manual Unmanaged/UnsafeMutableRawPointer C-callback trampoline the old
implementation needed.
WalletManager.networkIsReachable() -- called synchronously from the C
peer-manager's networkIsReachable callback, so it can't become async --
now owns a long-lived ReachabilityMonitor and just reads its isReachable
property instead of the deprecated SCNetworkReachabilityCreateWithAddress/
GetFlags pair.
Co-Authored-By: grunt-claude-bot <308083480+grunt-claude-bot@users.noreply.github.com>
Co-Authored-By: kcw-grunt <mrkerrywashington@icloud.com>
* fix: scope dangling raw-pointer usage in BRAddress and seed generation
UnsafeMutableRawPointer(mutating: &x) / UnsafeRawPointer([x]) are only
valid for the duration of the single call they're constructed in;
chaining .assumingMemoryBound(...) and using the result afterward (or,
worse, across several later calls) uses the pointer outside that
guaranteed scope -- undefined behavior the compiler now flags as
"results in a dangling pointer".
BRAddressExtension.swift: every init/accessor touching the address's raw
`s` buffer (init?(string:), init?(scriptPubKey:), init?(scriptSig:),
scriptPubKey, hash160, description) now does its C-interop call from
inside a withUnsafeBytes/withUnsafeMutableBytes(of:) closure instead of
a pointer built ahead of time.
WalletManager+Auth.swift: setRandomSeedPhrase()'s BIP39 entropy pointer
(entropyRef) was reused across SecRandomCopyBytes and two BRBIP39Encode
calls spanning several lines -- the most serious instance of this
pattern, since it's seed-entropy generation. Wrapped all three uses
inside a single withUnsafeMutableBytes(of: &entropy) closure.
Verified via BRAddressTests, WalletCreationTests, and
WalletAuthenticationTests (all pass) that behavior is unchanged.
Co-Authored-By: grunt-claude-bot <308083480+grunt-claude-bot@users.noreply.github.com>
Co-Authored-By: kcw-grunt <mrkerrywashington@icloud.com>
* test: add unit tests for BRAddressExtension and WalletManager+Auth
BRAddressExtensionTests: self-contained, deterministic coverage of
BRAddress's init(string:)/init(scriptPubKey:)/scriptPubKey/hash160/
description/Equatable/Hashable, using a hand-built P2PKH scriptPubKey
fixture rather than depending on wallet setup or the keychain.
WalletManagerAuthTests: covers setSeedPhrase, the seedPhrase(pin:) round
trip, changePin, pinLength, and wipeWallet -- business logic not already
exercised by WalletAuthenticationTests (pin lock-out) or WalletCreationTests
(random seed-phrase generation).
Both exercise the exact code paths touched by the preceding dangling-
pointer and SCNetworkReachability fixes, so they'll catch regressions
there going forward.
Co-Authored-By: grunt-claude-bot <308083480+grunt-claude-bot@users.noreply.github.com>
Co-Authored-By: kcw-grunt <mrkerrywashington@icloud.com>
* fix: migrate withUnsafeBytes/withUnsafeMutableBytes off deprecated typed-pointer overloads
Data.withUnsafeBytes/withUnsafeMutableBytes closures typed with a
concrete pointer type (e.g. (UnsafePointer<UInt8>) -> R) resolve to the
deprecated overload; the replacement overload's closure parameter is
UnsafeRawBufferPointer/UnsafeMutableRawBufferPointer. Migrated every
site across Extensions.swift (md5, base58 encode/decode, sha1/sha256,
uInt256, the uInt8/32/64(atOffset:) accessors, compactSign, genNonce,
the masterPubKey getter, BRKey.publicKey), BRKeyExtension.swift
(privKey/bip38Key), BRReplicatedKVStore.swift's genNonce duplicate, and
WalletManager+Auth.swift (creationTime load, BIP39 encode, apiAuthKey,
the generic Int64 keychainItem loader).
Where the underlying C function takes void* (BRMD5, BRSHA1/256,
BRKeyCompactSign, BRKeyPubKey) the fix is just .baseAddress; where it
takes a typed pointer (BRBase58Encode/Decode's char*/uint8_t*,
BRKeyPrivKey/BRKeyBIP38Key's char*) it's
.baseAddress?.assumingMemoryBound(to:); reinterpreting raw bytes as a
value (uInt256, the offset accessors, the Int64 keychain case) uses
.load(as:).
BRMasterPubKey.pubKey is a 33-byte C-array-as-tuple; calling the global
withUnsafeMutableBytes(of:) on that tuple projection crashed the type
checker ("failed to produce diagnostic for expression") instead of
compiling, so the masterPubKey getter instead uses
withUnsafeMutablePointer(to:) + withMemoryRebound(to: UInt8.self).
Co-Authored-By: grunt-claude-bot <308083480+grunt-claude-bot@users.noreply.github.com>
Co-Authored-By: kcw-grunt <mrkerrywashington@icloud.com>
* test: add ExtensionsCryptoTests for the Extensions.swift crypto/encoding helpers
md5, sha1/sha256, base58 encode/decode, uInt256, the uInt8/32/64(atOffset:)
accessors, and the BRMasterPubKey<->Data round trip had no test coverage
at all. Added known-vector and round-trip tests for each, verified
against the withUnsafeBytes/withUnsafeMutableBytes migration in the
preceding commit -- a mistake in that pointer plumbing would show up
here as a wrong hash rather than just a cleared warning.
Co-Authored-By: grunt-claude-bot <308083480+grunt-claude-bot@users.noreply.github.com>
Co-Authored-By: kcw-grunt <mrkerrywashington@icloud.com>
* fix: clear remaining deprecation warnings, badge count, and mock test scaffolding
- applicationIconBadgeNumber (deprecated iOS 17): WalletCoordinator now tracks
its own pending badge count (UserDefaults.pendingNotificationBadgeCount,
reset alongside AppDelegate's existing setBadgeCount(0) calls) since
UNUserNotificationCenter.setBadgeCount(_:) has no matching getter.
- FakeAuthenticator/testPublicKeyEncoding (BWAPIClientTests.swift): migrated
the remaining withUnsafeBytes/withUnsafeMutableBytes calls off the
deprecated typed-pointer overloads; apiAuthKey's BRKeyPrivKey call also
had the pointer-escapes-the-closure pattern fixed (matches the
BRKey.publicKey fix from earlier in this cleanup).
- MockURLSessionDataTask / MockURLSession: restated `@unchecked Sendable`
(required under Swift 6 concurrency checking when subclassing a Sendable
Foundation type) with comments explaining why each is safe.
- Removed MockURLSession.swift and MockURLSessionDataTask.swift entirely --
neither was ever actually instantiated (mockURLSession stayed nil), so
the URLSessionDataTask() deprecation inside them was dead code; deleting
it is the only real fix, since wrapping the call in a helper only
relocates the same warning rather than removing it (verified).
- Replaced the two remaining bare URLSessionDataTask() calls in
BWAPIClientTests.swift's redirect/challenge tests with the real
URLSession.dataTask(with:) factory method.
That last change gave testURLSession_Redirect_ToDifferentHost_DoesNotFollow
a real task for the first time, which exposed it as a false positive: it
only passed because a never-started bare task has a nil currentRequest,
not because the code under test actually blocked the redirect.
BWAPIClient.urlSession(_:task:willPerformHTTPRedirection:newRequest:...)
only checked the redirect's *origin* host against api.grunt.ltd, never the
*destination* -- so a redirect issued by our own API pointing anywhere
else would have been followed. Fixed both:
- BWAPIClient.swift now also requires the redirect's destination host/scheme
to match our own API before following it.
- Fixed the existing test to genuinely exercise that check, and added
testURLSession_Redirect_FromDifferentHost_DoesNotFollow (untrusted
origin) and testURLSession_Redirect_SameHost_Follows (positive case)
alongside it.
Also included: SignupAskViewTests' two branch-simplification fixes,
MockAuthenticationChallengeSender split into its own file, and removing
the long-dead "Run Script for CircleCI SPM Caching" build phase left over
from the earlier CircleCI removal.
Verified with a full clean build (0 errors, 0 warnings in every touched
file) and the full BWAPIClientTests/SignupAskViewTests suites (33 tests).
Co-Authored-By: kcw-grunt <mrkerrywashington@icloud.com>
Co-Authored-By: grunt-claude-bot <308083480+grunt-claude-bot@users.noreply.github.com>
---------
Co-authored-by: kcw-grunt <mrkerrywashington@icloud.com>
Co-authored-by: grunt-claude-bot <308083480+grunt-claude-bot@users.noreply.github.com>
…g the way (#175) * fix: remove dead code found while auditing warnings (UIView frame swizzle, MessageUIPresenter) UIView+FrameChangeBlocking.swift: swizzled UIView's frame setter app-wide at launch, gating writes on a per-instance isFrameChangeBlocked flag -- but nothing anywhere ever set that flag to true, so the swizzle had zero functional effect; every frame write just paid for an extra method-call hop through requestSetFrame(_:) for nothing. Its "equivalent of dispatch_once" comment was also stale/inaccurate: a bare static func has no actual once-guard, so a second call to swizzleSetFrame() would have silently un-swizzled it via method_exchangeImplementations. MessageUIPresenter: instantiated once in ModalPresenter (messagePresenter), but that property was never referenced again anywhere -- none of presentMailCompose/presentFeedbackCompose/presentSupportCompose were ever called. Removed both entirely, including their pbxproj file/build-phase entries. Co-Authored-By: grunt-claude-bot <308083480+grunt-claude-bot@users.noreply.github.com> Co-Authored-By: kcw-grunt <mrkerrywashington@icloud.com> * fix: stop performRequestWithRetry from recursing unconditionally attemptRequest() called itself immediately after firing its URLSession data task, outside the retry-on-failure branch -- so every call recursed synchronously before any network response could return, regardless of whether the request actually needed retrying. This was flagged during an earlier warnings review (the compiler's "function call causes an infinite recursion" diagnostic on this same function) as a critical bug distinct from the review's warning cleanup; fixing it here. Co-Authored-By: grunt-claude-bot <308083480+grunt-claude-bot@users.noreply.github.com> Co-Authored-By: kcw-grunt <mrkerrywashington@icloud.com> * fix: small Swift-safety cleanups in Legacy_BW_BRClasses - BRMasterKeyExtension.swift: mark the retroactive Equatable conformance on BRMasterPubKey (an imported BRCore type) with @retroactive, per the compiler warning that this won't behave correctly if BRCore itself adds the conformance in the future. - BRReplicatedKVStore.swift: replace a force-cast (as!) with a safe optional cast (as?) when handing a caught error to completionHandler. - BRTxInputExtension.swift: line-wrap reformatting only, no behavior change. Co-Authored-By: grunt-claude-bot <308083480+grunt-claude-bot@users.noreply.github.com> Co-Authored-By: kcw-grunt <mrkerrywashington@icloud.com> * fix: scope dangling pointers and migrate deprecated Security APIs in PaymentProtocolRequest certs/digest: UnsafeMutablePointer(mutating:) on a Swift array/let escaped its guaranteed-valid scope (same pattern fixed elsewhere this cleanup); now scoped via withUnsafeMutableBufferPointer. isValid()'s certificate-trust and signature-verification flow: - SecTrustEvaluate + SecTrustCopyProperties (manual error-string extraction from a loosely-typed properties array) -> SecTrustEvaluateWithError, which returns the trust decision and a descriptive CFError in one call. - SecTrustCopyPublicKey -> SecTrustCopyKey (direct rename). - SecKeyRawVerify(_, .PKCS1SHA256/.PKCS1SHA1, ...) -> SecKeyVerifySignature(_, .rsaSignatureDigestPKCS1v15SHA256/SHA1, ...), the documented replacement for verifying a PKCS1v15 signature over an already-hashed digest. The errSecUnimplemented-sentinel trick for distinguishing "unsupported pkiType" from "verification failed" is now an explicit pkiType check instead of relying on an OSStatus that was never reassigned. Verified via full clean build (0 errors/warnings in this file) and the existing PaymentRequestTests suite. isValid()'s trust/signature path itself has no direct automated coverage (pre-existing gap, not introduced here) -- building real fixtures for it would need a self-signed cert and BRTxOutput construction, worth its own task. Co-Authored-By: grunt-claude-bot <308083480+grunt-claude-bot@users.noreply.github.com> Co-Authored-By: kcw-grunt <mrkerrywashington@icloud.com> * fix: migrate userAccount keychain storage off deprecated NSKeyedArchiver APIs The generic keychainItem<T>/setKeychainItem<T> pair's [AnyHashable: Any] case (backing WalletManager.userAccount) used the deprecated NSKeyedArchiver.archivedData(withRootObject:) / NSKeyedUnarchiver.unarchiveObject(with:). Both functions already throw, so: - write: archivedData(withRootObject:requiringSecureCoding: true) - read: unarchivedObject(ofClasses:from:) with an explicit allowlist of the plist-compatible classes a userAccount blob can actually contain (NSDictionary/NSArray/NSString/NSNumber/NSDate/NSData/NSNull) userAccount had no test coverage at all; added a round-trip test and a nil-before-set test to WalletManagerAuthTests. Co-Authored-By: grunt-claude-bot <308083480+grunt-claude-bot@users.noreply.github.com> Co-Authored-By: kcw-grunt <mrkerrywashington@icloud.com> --------- Co-authored-by: kcw-grunt <mrkerrywashington@icloud.com> Co-authored-by: grunt-claude-bot <308083480+grunt-claude-bot@users.noreply.github.com>
Co-authored-by: kcw-grunt <mrkerrywashington@icloud.com> Co-authored-by: grunt-claude-bot <308083480+grunt-claude-bot@users.noreply.github.com>
- Header now shows an explicit close button alongside centered title - QR/address block shows the full address (no truncation), centered "COPY NEW ADDRESS" affordance with a bordered copy icon - Currency wheel picker shows more surrounding rows, has its default row background stripped (UIPickerView+Extension.swift), and brackets the selected row with hairlines - Preset amount segmented control replaced with bordered pill chips (min / 10x / max / Custom), Custom reveals inline amount entry - Buy button unified into a single lavender rounded rect with the MoonPay attribution folded in, relabeled BUY LTC - All content now sits in a single BrainwalletColor.surface container sized 90% width / 60%[->75%] height of the sheet - Sheet itself now presented as translucent (presentationBackground + a dialed-down VariableBlurView, since ultraThinMaterial + opacity only fades the blur rather than thinning it) Co-authored-by: kcw-grunt <mrkerrywashington@icloud.com> Co-authored-by: grunt-claude-bot <308083480+grunt-claude-bot@users.noreply.github.com>
"GET LTC" and "Set amount:" no longer appear anywhere in source (BuyReceiveView relayout renamed the buy button to "BUY LTC" and replaced the standalone "Set amount:" row with the preset chips / inline Custom entry), so their catalog entries were dead weight. Co-Authored-By: grunt-claude-bot <308083480+grunt-claude-bot@users.noreply.github.com> Co-Authored-By: kcw-grunt <mrkerrywashington@icloud.com>
json.dump's default separators omit the space before each colon that
Xcode's String Catalog serializer always writes ("key" : value vs
"key": value). That mismatch meant every auto-translate run rewrote
every line of the ~37k-line file, even when only a couple of strings
actually changed — producing unreviewable, unmergeable PRs and
spawning a fresh one on every push that touched the file (#159, #160,
#164, #165, #167-170, #178, #180, #182, #183, #185, ...).
Verified the fix is idempotent: round-tripping the current
Localizable.xcstrings through load/save now produces a byte-identical
file.
Co-Authored-By: grunt-claude-bot <noreply@grunt.ltd>
Co-Authored-By: kcw-grunt <kerry@grunt.ltd>
…zable-keys chore: remove unused Localizable.xcstrings keys
fix: match Xcode's JSON formatting when writing Localizable.xcstrings
Cherry-picked from release/v3.9.17, where this string had full translations but was empty on develop. Co-Authored-By: grunt-claude-bot <noreply@grunt.ltd> Co-Authored-By: kcw-grunt <kerry@grunt.ltd>
Update game and restore COPY NEW ADDRESS translations
) Dropped the pull_request trigger. It re-fired the workflow on every commit pushed to any PR touching Localizable.xcstrings — including long-lived release branches (4 translation PRs spawned against release/v3.9.16-into-main in one afternoon) and the bot's own PR check runs (#188, opened against the '184/merge' ref while #184 was still under review, duplicating #189's content once #184 actually merged). Translations only need generating once a change actually lands on main/develop, not during PR review, so push-only is sufficient and removes the self-duplicating behavior. Also drops the now-dead github.event.pull_request.* fallbacks that existed only to support that trigger. Co-authored-by: kcw-grunt <mrkerrywashington@icloud.com> Co-authored-by: grunt-claude-bot <noreply@grunt.ltd> Co-authored-by: kcw-grunt <kerry@grunt.ltd>
Languages: ar, de, es-419, fa-IR, fr, hi, id, it, ja, ko, pa, pl, pt-BR, nl, ru, sv, th, tr, uk, zh-Hans, zh-Hant Triggered by: 7fe008e Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
## Highlights since v3.9.16 - Reworked the Buy/Receive sheet (BuyReceiveView) to match the Android app's layout (#177) - Cleared remaining deprecated Security/Foundation API usage and a real infinite-recursion bug (#175) - Reduced the Swift compiler warnings list (#174) - Removed CircleCI — Xcode Cloud is now the sole CI (#173) - Updated Private/bw-gdlib to v1.6.9 (#171) and Modules/core (#176) - Restored the missing translations for COPY NEW ADDRESS, and added translations for BUY / RECEIVE and POWERED BY MOONPAY (21 locales each) — these existed in an earlier release attempt but were never merged back into develop (#184) - Removed 2 dead Localizable.xcstrings keys with no remaining source references: GET LTC, Set amount: (#179) - Fixed the auto-translate script writing Localizable.xcstrings with JSON formatting that didn't match Xcode's String Catalog serializer, which was rewriting every line of the file on every run and producing unreviewable, unmergeable translation PRs (#186) - Fixed the auto-translate workflow re-triggering on every push to any open PR (not just merges to main/develop), which was spawning duplicate translation PRs against long-lived branches (#190) - Picked up 42 additional previously-missing translations across 21 locales, generated cleanly after the above two fixes (#189) - Bumped MARKETING_VERSION to 3.9.17 ## Build CURRENT_PROJECT_VERSION bumped 2602309 -> 2602311. Co-Authored-By: grunt-claude-bot <noreply@grunt.ltd> Co-Authored-By: kcw-grunt <kerry@grunt.ltd>
main and develop have zero shared git history (a past history-squash severed them), so GitHub refuses to compare/PR release/v3.9.17 directly against main (404 'No common ancestor'). This -s ours merge records main's current tip (a9e7cee, Release v3.9.16 #166) as a second parent without changing any file content, which is enough for GitHub to accept the PR. Co-Authored-By: grunt-claude-bot <noreply@grunt.ltd> Co-Authored-By: kcw-grunt <kerry@grunt.ltd>
Collaborator
Author
|
Superseding with a clean, direct release/v3.9.17 → main PR now that #192 gave develop real shared ancestry with main — the -s ours bridge branch this PR used is no longer needed. See the replacement PR. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Release v3.9.17
BuyReceiveView) to match the Android app's layout (Relayout BuyReceiveView to match Android BUY/RECEIVE sheet #177)Private/bw-gdlibto v1.6.9 (chore: update bw-gdlib submodule to v1.6.9 #171) andModules/core(Update core #176)COPY NEW ADDRESS, and added translations forBUY / RECEIVEandPOWERED BY MOONPAY(21 locales each) (Update game and restore COPY NEW ADDRESS translations #184)Localizable.xcstringskeys with no remaining source references:GET LTC,Set amount:(chore: remove unused Localizable.xcstrings keys #179)MARKETING_VERSIONto 3.9.17,CURRENT_PROJECT_VERSIONto 2602311v3.9.17mainanddevelophave zero shared git history (a past history-squash severed them), which is why GitHub can't even compute a diff between them directly (404 No common ancestor). This branch carries a-s oursmerge recordingmain's current tip as a second parent — content-identical torelease/v3.9.17, but with real ancestry — just to get this PR to exist at all.Squash-merging would discard that second parent and leave
main/developdisjoint forever, repeating this same problem on every future release.Merging with "Create a merge commit" instead permanently welds the two histories together at this commit. After that,
developshould be able to fast-forward cleanly ontomain, and every futurerelease/vX.Y.Zbranched fromdevelopwill PR intomainwith just that release's own commits — no more bridge branches needed.Co-Authored-By: grunt-claude-bot noreply@grunt.ltd
Co-Authored-By: kcw-grunt kerry@grunt.ltd
🤖 Generated with Claude Code