Cloudkit deploy schema
Skill jimmynotjames/simple-recurring-budgets/.cursor/skills/cloudkit-deploy-schema
Deploy this app's CloudKit record schema from the Development environment to Production, safely — includes preflight checks and mandatory human-confirmation gates before anything destructive or production-affecting. Use when promoting a new or changed CloudKit schema to Production (including the first-ever/greenfield deploy for a new container), or when asked to update/sync the Production CloudKit schema.From its SKILL.md
npx -y skills add jimmynotjames/simple-recurring-budgets --skill cloudkit-deploy-schemaAssembled from the repository path, not quoted from the project. Check it against their README if it does not work.
2 things to look at
- no licenseNo license file was found in the repository. Code published without one is not open source by default, so using it at work is a question for whoever answers licensing questions where you are.
- 0 stars0 stars. Stars are a popularity signal and not a quality one, but at this level it is likely that nobody has read this closely except its author, and you would be relying on your own review.
SKILL.md
15.2 KB, ~3.5k tokens by cl100k_base, as published. Nobody here has run it
Deploy CloudKit schema to Production
A narrow, safety-first operational skill for one specific task: getting this
app's CloudKit schema (record types, fields, indexes — never data) from
the Development environment into Production. Production is what TestFlight
and App Store builds actually use (see docs/tech-design-doc.md §4.3), so an
undeployed or wrong schema means sync silently fails for exactly the builds
that matter, with no visible error to the end user.
This skill is deliberately conservative. CloudKit Production schema
changes are (practically) one-directional and permanent: fields can be
added to Production at any time, but cannot be deleted once deployed
(docs/tech-design-doc.md §4.3). A careless deploy bakes in cruft forever.
Treat every deploy — the first one and every subsequent one — as something to
slow down for, not a routine mechanical step.
Model preference
Run this skill on the latest Opus-tier model (or its contemporary
equivalent), not Sonnet. Unlike the checklist-walking orchestration in
appstore-prepare-for-release (deliberately Sonnet — mechanical steps, no
linguistic/architectural judgment), this skill's entire job is judgment:
deciding whether a schema diff is intentional or stale cruft, whether a field
removed from the Swift model was ever actually synced to Development, and
whether a given action is the safe direction or the destructive one. The
consequence of a wrong call here is a permanent, unfixable mutation to a
production system serving real users — that is exactly the profile of a
release-checklist item worth the extra reasoning depth (same rationale as
appstore-prepare-for-release's P1.6/P1.7/P1.8/P6.1 escalations, which this
skill supersedes for schema work specifically at P3.7). If invoked from a
Sonnet-orchestrated session (e.g. mid-checklist), spawn this skill's work on
an Opus subagent rather than running it inline on Sonnet.
Hard rules
- Never treat this as a fire-and-forget script. Every step below that
writes anything (Reset Environment, Deploy Schema Changes,
cktool import-schemato either environment) requires an explicit, freshly-stated human confirmation immediately before running it — even if the user approved the overall plan earlier in the conversation. Re-confirm at the point of action, not just at the start. - "Reset Environment" and "Deploy Schema Changes" are opposite directions —
never conflate them.
- Reset Environment (Development only) wipes Development's schema and data back to match whatever is currently in Production. If Production is empty or behind Development, this is destructive to Development's working state.
- Deploy Schema Changes (Development → Production) is additive-only: it pushes Development's current schema to Production. It never touches data in either direction.
- Before either action, state out loud (to the user) which direction you believe you're going and why, and get their explicit go-ahead.
- Never hand-author or hand-edit a CKSL (
.ckdb) file's field types from guesswork about how SwiftData encodes a property. SwiftData's CloudKit mirroring has its own internal encoding (e.g. forDecimal, optional arrays, nestedCodablestructs) that isn't safe to reverse-engineer. The reliable source of truth for "what should the schema contain" is the Development schema as generated by the app's own real persistence code — never a schema file typed up by hand. - Data and schema are entirely separate stores. Clearing/reviewing Development data has zero effect on what a schema deploy pushes to Production, and vice versa. Don't conflate a data-hygiene concern with a schema-deploy concern.
- The CloudKit Console's "Deploy Schema Changes" button is the only way
to promote a schema to Production — there is no
cktoolequivalent, despite appearances.cktool import-schemaaccepts--environment productionas a syntactically valid flag value (and Apple's own man page lists it without caveat), but the server-side endpoint rejects it at request time:BadRequestException: endpoint not applicable in the environment 'production'(confirmed empirically, not just documented — don't trust the flag's accepted-values list over what the API actually does).import-schemaonly actually works against--environment development. Budget for a manual Console click at the deploy step every time; don't try to script around it.
Preflight checks (every invocation)
Run these before touching anything, and report the results before asking for any confirmation:
- Identify the container and team. Read
com.apple.developer.icloud-container-identifiersfromsimple-recurring-budgets/Resources/simple_recurring_budgets.entitlementsfor the container id; the team id comes from the Apple Developer account in use (ask the user if not obvious fromfastlane/.envor Xcode settings). - Confirm
cktoolauth. Schema management commands need a Management Token (different from the ASC API key used elsewhere in this repo):
This is interactive (opens a browser/prompts for the token) — tell the user to run it themselves if not already authenticated; don't attempt to script around it.xcrun cktool save-token --type management - Snapshot both environments before changing anything:
Show the user a summary of each (record types present, field counts) and the diff between them. This is your evidence base for every subsequent question — don't ask the user to characterize the current state from memory when you can just read it.xcrun cktool export-schema --team-id <TEAM> --container-id <CONTAINER> --environment development --output-file tmp/schema-dev.ckdb xcrun cktool export-schema --team-id <TEAM> --container-id <CONTAINER> --environment production --output-file tmp/schema-prod.ckdb - Full bidirectional model-vs-schema field audit — mandatory before every
deploy, not optional. Cross-reference the Development schema (exported
above) against the current Swift model source
(
simple-recurring-budgets/Models/*.swift) in both directions and present the result as a table (one row per@Modelclass):- Stale fields (in the CloudKit schema, absent from the current
model): a candidate for removal from Development before deploying —
see "Handling stale fields" below. Don't rely on memory of what used to
exist;
git log -p -- path/to/Model.swiftis the authoritative record. - Missing fields (a stored property on the current model with no
corresponding
CD_<field>in the exported Development schema): this means the field never got a non-nil value written anywhere in whatever generated the current Development data, so CloudKit never created it — it will silently be absent from Production too if you deploy now. This is the more dangerous direction to miss, because it doesn't show up as a "diff" against anything; it's an absence. Block the deploy and go back to generating more complete Development data (see step 2 in Recipe A, or the equivalent regeneration for Recipe B) until every stored property has a corresponding field. - When listing "current model" properties, get the list directly from
each
@Modelclass's stored properties (var/letdeclarations that aren't computed), not from memory or from what a test/preview happens to touch. Exclude the model's own to-many relationship arrays from the "should appear on this record type" expectation — SwiftData represents a to-many relationship via the inverse foreign key (CD_<parentType>) on the child record type, not as a field on the parent. Confirm each relationship's inverse key is present on the child type instead of expecting it on the parent. - Do this for every
@Modelclass in the schema'smodelsarray (check theVersionedSchemaconformer, e.g.SchemaV1.swift, for the authoritative list), not just the ones that seem most relevant.
- Stale fields (in the CloudKit schema, absent from the current
model): a candidate for removal from Development before deploying —
see "Handling stale fields" below. Don't rely on memory of what used to
exist;
Recipe
A. First-ever ("greenfield") deploy for a container
Use when Production's schema is empty or near-empty (only Apple's built-in
system record types, e.g. Users) and this is the first time this
container's schema is being established.
- Run the preflight checks above. Confirm with the user that Production is indeed empty/greenfield — don't assume it from a prior conversation turn.
- Generate a complete, deterministic Development schema from real app
code — do not rely on ad-hoc manual UI exercise, which risks missing a
field. The reliable path:
- Check whether a comprehensive fixture/seed routine already exists in
the codebase (e.g.
DebugData.seed(into:)or equivalent — search for#if DEBUGseed helpers underModels/orPreviews/). If one exists and its coverage of model fields looks complete (cross-check against the current model's stored properties), prefer reusing it. - Wire it to a throwaway trigger (a
#if DEBUG-gated button, or a temporary call in the app-launch path) on a separate throwaway git branch that is never merged to main — never commit temporary seed-wiring to a real feature branch or main. - Have the user run a plain Debug/Development-signed build (Xcode Run, simulator or physical device — what matters is Development code signing, not device vs. simulator) signed into a real iCloud account with network on, and trigger the seed once.
- Confirm the seed actually reached CloudKit (not just the local SwiftData
store) — ask the user to check the CloudKit Console's Development data
browser for the new records, or re-run
export-schema --environment developmentand confirm the expected record types/fields now appear. - Don't delete the throwaway branch yet — keep it until the deploy is verified (step 5/7 below), in case a gap surfaces in the audit and you need to re-seed.
- Check whether a comprehensive fixture/seed routine already exists in
the codebase (e.g.
- Re-export the Development schema and re-run the full field audit from preflight check 4 (not just a diff against empty Production — every model field must be confirmed present, not just "different from before"). Present the audit table and the diff against the empty Production baseline to the user. Explicit confirmation required before proceeding.
- Deploy: tell the user to go to CloudKit Console → container → Schema →
Deploy Schema Changes. This step is manual, always —
cktool import-schema --environment productionis not a real alternative (the API rejects it; see the hard rule above). Have the user confirm once they've clicked through the Console's diff preview and committed. - Verify:
export-schema --environment productionagain, confirm it now matches Development's record types/fields. - Record what was deployed (record types, field count, date, method used)
in the release checklist snapshot's P3.7 item (see
docs/app-store-release-checklist.md) and/ordocs/tech-design-doc.md§4.3 if the constraint notes need updating. - Clean up the throwaway branch now that the deploy is verified — don't
leave it lying around "just in case." Confirm its only commits are the
temporary seed-wiring (
git log --oneline main..<branch>, sanity-check the diff touches only the throwaway trigger, nothing else), then delete it:
(force-delete, since it was deliberately never merged — a plaingit branch -D <throwaway-branch>-dwill refuse). If it was ever pushed to a remote, delete it there too (git push origin --delete <throwaway-branch>) — though per the rule above it should have stayed local-only.
B. Incremental deploy (schema already established in Production)
Use for any release after the first, when the SwiftData model has changed (new fields/record types) since the last deploy.
- Run the preflight checks. The diff between the two exported schemas is the change set — present it to the user explicitly, field by field.
- If preflight check 4 flags any "missing fields" (a new stored property
on the model with no corresponding field yet in Development — the normal
case right after adding a field to an
@Modelclass, before any record has ever been saved with a non-nil value for it), the same problem as Recipe A step 2 applies: don't guess, generate real data. Follow that step's method — reuse/extend a#if DEBUGseed fixture on a throwaway branch, run a Debug build signed into a real iCloud account, confirm the field materializes via a re-export — before proceeding. Skip this step only if the audit found no missing fields. - Confirm every new field in the diff is intentional (traces to an actual
model change, not stale cruft) — cross-check against
git logfor the relevant model files if anything looks unexpected. - Explicit confirmation required before deploying, quoting the exact diff being promoted.
- Deploy: tell the user to go to CloudKit Console → container → Schema → Deploy Schema Changes (manual, always — see the hard rule above).
- Verify with a post-deploy
export-schema --environment production. - Record the deploy in the current release's checklist snapshot P3.7 item.
- If step 2 required a throwaway seeding branch, clean it up now per
Recipe A step 7 (sanity-check its commits, then
git branch -D).
Handling stale fields in Development
If the preflight check finds Development schema fields with no corresponding property on the current model (e.g. a renamed or removed field from an earlier development iteration):
- These can be deleted directly in the CloudKit Console (Development schema, unlike Production, supports field deletion) — confirm with the user which specific fields to remove, one by one, before doing so.
- Never use Reset Environment as a shortcut to "clean up" Development stale fields unless Production is confirmed empty/behind — Reset Environment wipes Development back to match Production's current state, which is destructive if Production isn't already a superset of what you want to keep.
- Removing a stale field from Development has no effect on Production unless it was already deployed there — in which case it's stuck permanently (Production can't lose fields) and the right move is just to stop adding data to it going forward, not to attempt removal.
Where this fits in a release
This skill is invoked from the App Store release checklist's P3.7
(docs/app-store-release-checklist.md, driven by /appstore:prepare-for-release) —
that item's Note should reference back to what this skill did (deploy method,
fields involved, verification result) rather than duplicating the full
recipe.
What ships with it
Read from the repository
Just SKILL.md. No reference files, no scripts.