Skip to content

Stop the Lite manifest from claiming tracking it cannot name - #134

Merged
andiwand merged 1 commit into
mainfrom
fix-lite-privacy-manifest-tracking
Aug 3, 2026
Merged

Stop the Lite manifest from claiming tracking it cannot name#134
andiwand merged 1 commit into
mainfrom
fix-lite-privacy-manifest-tracking

Conversation

@andiwand

@andiwand andiwand commented Aug 3, 2026

Copy link
Copy Markdown
Member

Build 43 of the Lite app came back ITMS-91064 / Invalid Binary: its PrivacyInfo.xcprivacy set NSPrivacyTracking to true while NSPrivacyTrackingDomains was empty, which App Review rejects. Pro passed the same upload with false and an empty list, so this mirrors it.

The manifests only landed in #107, after 1.35 shipped, so this file had never been through App Store validation before — 1.37 build 43 was its first upload.

Why not just list the AdMob domains

That would satisfy the rule but cost more than it buys. iOS blocks the domains named in NSPrivacyTrackingDomains whenever ATT authorization is missing, so listing the AdMob endpoints would also cut off the non-personalized ads served to users who decline the prompt. The Google SDKs ship no domains of their own for that reason, and GoogleMobileAds already marks DeviceID as NSPrivacyCollectedDataTypeTracking in its own manifest, which Apple aggregates with this one. The tracking disclosure is still made.

Verified against the shipped artifacts from run 30767906998, not just the source:

build 43 NSPrivacyTracking NSPrivacyTrackingDomains outcome
Pro false empty passed
Lite true empty ITMS-91064

Also

Corrects 1.37.txt to the copy actually submitted. 1.36 was tagged but never reached the store, so App Store Connect jumps 1.35 → 1.37 and the notes have to cover the 1.36 work.

Pro 1.37 (build 43) is unaffected and still Waiting for Review. Lite needs a new build from this branch.

🤖 Generated with Claude Code

Build 43 of the Lite app came back ITMS-91064 Invalid Binary: its
PrivacyInfo.xcprivacy set NSPrivacyTracking to true while
NSPrivacyTrackingDomains was empty, which App Review rejects. The Pro
manifest passed the same upload with false and an empty list.

The manifests only landed in #107, after 1.35 shipped, so this file had
never been through App Store validation before.

Listing the AdMob endpoints would satisfy the rule but cost more than it
buys: iOS blocks the domains named here whenever ATT authorization is
missing, so it would also cut off the non-personalized ads served to
users who decline. The Google SDKs ship no domains of their own for that
reason, and GoogleMobileAds already marks DeviceID as tracking in its own
manifest, which Apple aggregates with this one.

Also corrects the 1.37 release notes to the copy actually submitted:
1.36 was tagged but never went to the store, so the store jumps 1.35 to
1.37 and the notes have to cover the 1.36 work.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019sHnwXJEjogZDLATrBgMQS
@andiwand
andiwand merged commit 4e4a2ce into main Aug 3, 2026
5 checks passed
@andiwand
andiwand deleted the fix-lite-privacy-manifest-tracking branch August 3, 2026 21:05
andiwand added a commit that referenced this pull request Aug 6, 2026
* Tag what a release built instead of what it was meant to build

A version tag had to be pushed before the build it named, which is a
promise the release cannot keep: a version often takes more than one
build to clear review, and the second one is built from a different
commit. Tag 1.37 is the case in point - it points at 6e97420, three
commits behind what was actually submitted, and the build that shipped
has no name in git at all.

So the tag stops being the trigger and becomes the record. The workflow
is dispatched by hand with the version it should build, and writes
build/<flavor>/<version>/<build> at the commit it built once the upload
goes through. Per flavor, because Pro and Lite count their builds
separately: one commit uploaded to both apps is two builds with two
different numbers. The plain version tag is left to a human, once the
release is actually live.

The build number was only ever known inside the lane, so the Fastfile
hands it back through GITHUB_OUTPUT. Dropping the tag trigger also
matters on its own: the receipt tag would have matched '[0-9]*' and
started another release.

resolve-version.py loses its tag arm, and the version input is now the
only source. Nobody bumps a version in the tree - a commit on main is
not a release, and the 0.0.0 in project.pbxproj says so.

CHANGELOG.md gets the matching rule: the heading is cut at submission
rather than at a tag, on main, in the same pull request that writes the
store copy, and a fix that goes up as a second build of the same version
belongs under the heading already cut rather than back under Unreleased.
That is the case #134 fell into.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>\

* Build once, upload per app, and record what actually shipped

Follows the same change in OpenDocument.droid. The release workflow was a matrix
over a `flavor` input, pinned to max-parallel 1, so one checkout, one Xcode
select, one ruby setup, one keychain import and one version resolve all ran
twice on two macOS runners.

Both apps always go out together now, and nothing chooses one. Pro and Lite are
the same app with ads and tracking switched off, so anything worth rebuilding one
for is worth rebuilding the other for.

Build, upload and record are three jobs. A half uploaded release is repaired with
"Re-run failed jobs", which retries only the failed upload against the .ipa
already built and signed - build number included, since it is baked in at archive
time. That is what makes the next part possible.

Pro and Lite now share a build number: one above the highest either app has,
resolved once and given to both builds. So one (version, build) pair names one
commit in both listings, and the build tag loses its per-flavor component. Asking
App Store Connect once rather than once per app is required, not just tidier -
querying again after Pro's upload would hand Lite a higher number.

The Fastfile splits `deploy` into build_ipa and upload_ipa, with buildPro /
uploadPro / resolveBuildNumber lanes for the workflow. deployPro and deployLite
still build and upload in one go, so a hand run off a laptop is unchanged.

The version tag is no longer written by hand. `record` drafts a github release at
the built commit; a draft creates no tag, and publishing it creates one there. A
version that needs a second build to clear review re-points the same draft.

The release body is the version's CHANGELOG.md section with the generated pull
request list below it, and a version with no section is refused before anything is
built rather than after both apps are uploaded.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* Say the same things in fewer words

The release design is explained three times over - once in the workflow
header, once in the README, once in each script - and each telling had
grown to the length of an essay. Cut the repetition and the reflow-only
churn, keeping the reasons that are not visible from the code: why the
build number is resolved once, why record re-derives the version, why a
draft rather than a tag.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
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