fix(sync): deploy the missing record types and gate every CloudKit write on the production schema - #2226
Merged
Merged
Conversation
…ite on the production schema
|
You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard. |
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.
Account settings showed a sync error that came back on every attempt:
Root cause
The app pushed three CKRecord types that were never deployed to the Production CloudKit schema:
FavoriteTable,SQLFavoriteandSQLFavoriteFolder. CloudKit only auto-creates types and fields in the Development environment, and both apps pin the container to Production, so no build could ever create them. A rejected record never clears its dirty flag, so it was retried on every sync forever.The guard that should have caught this only covered one record type.
ConnectionSyncFieldgated fields onConnection, andProductionSchemaParityTestsonly ever parsed theConnectionblock. Every other record type wrote its fields by raw string, so nothing was checked.The same bug was live in three more places, found while tracing this one:
SSHProfile.jumpHostsJsonwas written but never deployed, so any SSH profile with a jump host was rejected whole.Connection.tagIds,isFavorite,aiRulesandaiAlwaysAllowedToolswere declared but gated off, so they silently never synced. A connection with several tags arrived with only the first one.Connection.queryTimeoutSeconds, written by the iOS app, was gated off the same way.Server change
All three record types and all six fields are deployed to Production.
cktoolcan only write the Development environment (validate-schema --environment productionreturns "endpoint not applicable in the environment 'production'"), so Development was imported withcktool import-schemaand deployed to Production through CloudKit Console.CloudKit/production-schema.ckdbis the re-export.Code change
The point of this half is that a raw-string write can no longer bypass the gate.
SyncSchemaFieldenum, one per record type. Writes go throughrecord.fields(SomeSyncField.self), whose subscript refuses any field that is not verified against the deployed schema.SyncMapperFieldAccessTestsscans them forrecord["and fails on a match, so the ungated path cannot come back.SyncRecordType.verifiedInProductiongates whole record types, andCloudKitSyncEngine.pushwithholds any record whose type is not verified instead of letting the server reject it and retry forever. That is the single chokepoint both the Mac and iOS apps push through.default:arm, so a case added without being listed is inert rather than destructive.ProductionSchemaParityTestsnow checks every record type and every field against the snapshot in both directions: it fails when the app would write something undeployed, and when the gate is needlessly withholding something that is deployed.Verification
SyncRecordMapper*,SyncChangeTracker,SyncScope,EntitlementsEnvironmentParity).swiftlint --strictclean on every changed path. One pre-existing violation inExecutionAuditLog.swiftis untouched by this branch.FavoriteTablein the snapshot makes it fail withmissing → ["FavoriteTable"], and deletingjumpHostsJsonmakes it fail withmissing → ["jumpHostsJson"]against.sshProfile. Both name the record type and the deploy steps. The snapshot was restored and re-exported afterwards.Tests changed rather than added
Three existing tests asserted the deployment gap in their own names (
isFavorite stays off the wire until the production schema is verified,Writes legacy tagId only while tagIds is unverified in the production schema,A gated field does not survive the round trip). Their premise is no longer true, so they now assert the fields reach the wire and round-trip.One fixture changed:
CloudKitSyncEngineTests.pushThrowspushed a record of placeholder type"Test", which the gate correctly withholds before the entitlement check runs. It now uses a real record type, so it still exercises the entitlement path, and a separate test covers the withheld-record short circuit.