Skip to content

fix(sync): deploy the missing record types and gate every CloudKit write on the production schema - #2226

Merged
datlechin merged 1 commit into
mainfrom
fix/cloudkit-production-schema-parity
Aug 19, 2026
Merged

fix(sync): deploy the missing record types and gate every CloudKit write on the production schema#2226
datlechin merged 1 commit into
mainfrom
fix/cloudkit-production-schema-parity

Conversation

@datlechin

Copy link
Copy Markdown
Member

Account settings showed a sync error that came back on every attempt:

iCloud rejected 1 change(s). They stay on this device and will retry: Error saving record ... to server: Cannot create new type FavoriteTable in production schema

Root cause

The app pushed three CKRecord types that were never deployed to the Production CloudKit schema: FavoriteTable, SQLFavorite and SQLFavoriteFolder. 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. ConnectionSyncField gated fields on Connection, and ProductionSchemaParityTests only ever parsed the Connection block. 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.jumpHostsJson was written but never deployed, so any SSH profile with a jump host was rejected whole.
  • Connection.tagIds, isFavorite, aiRules and aiAlwaysAllowedTools were 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. cktool can only write the Development environment (validate-schema --environment production returns "endpoint not applicable in the environment 'production'"), so Development was imported with cktool import-schema and deployed to Production through CloudKit Console. CloudKit/production-schema.ckdb is the re-export.

Code change

The point of this half is that a raw-string write can no longer bypass the gate.

  • Every wire key is now declared by a SyncSchemaField enum, one per record type. Writes go through record.fields(SomeSyncField.self), whose subscript refuses any field that is not verified against the deployed schema.
  • Both mappers were converted off raw-string subscripts. SyncMapperFieldAccessTests scans them for record[" and fails on a match, so the ungated path cannot come back.
  • SyncRecordType.verifiedInProduction gates whole record types, and CloudKitSyncEngine.push withholds 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.
  • Verification is a membership set rather than a default: arm, so a case added without being listed is inert rather than destructive.
  • ProductionSchemaParityTests now 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

  • Package tests: 87 pass in 13 suites.
  • App sync suites pass (SyncRecordMapper*, SyncChangeTracker, SyncScope, EntitlementsEnvironmentParity).
  • macOS app builds.
  • swiftlint --strict clean on every changed path. One pre-existing violation in ExecutionAuditLog.swift is untouched by this branch.
  • The new guard was mutation tested rather than assumed: renaming FavoriteTable in the snapshot makes it fail with missing → ["FavoriteTable"], and deleting jumpHostsJson makes it fail with missing → ["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.pushThrows pushed 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.

@chatgpt-codex-connector

Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.

@datlechin
datlechin merged commit f6c9ea8 into main Aug 19, 2026
6 of 7 checks passed
@datlechin
datlechin deleted the fix/cloudkit-production-schema-parity branch August 19, 2026 06:29
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant