8000 Harden external TestFlight distribution by shroominic · Pull Request #29 · shroominic/OpenGlucose · GitHub
[go: up one dir, main page]

Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
69 changes: 56 additions & 13 deletions docs/releases.md
Original file line number Diff line number Diff line change
Expand Up @@ -91,16 +91,55 @@ exact valid build is associated only with that one internal group and has no
individually assigned testers, then repeats the audience checks before success.
It never calls the external association or notification lanes.

External mode requires `DISTRIBUTE_EXTERNAL=yes`. It
rejects every internal or external group that automatically receives all
builds, resolves exactly one existing external TestFlight group, and records
its immutable ID. It rechecks that audience immediately before it uploads
with distribution and automatic notification disabled, associates the exact
processed build through that ID, proves that no other external group is
associated, rejects every other internal/external group and individually
assigned tester on the build, and only then sends a single build-scoped tester
notification. Before that non-idempotent request, it durably creates a mode-600
`pending` claim containing the exact source commit, build, and group identity at
External mode requires both `DISTRIBUTE_EXTERNAL=yes` and
`DISTRIBUTE_INTERNAL=yes` because Apple's approved automatic internal group
also receives every App Store-eligible build. It pins that group and its sole
tester by immutable IDs, rejects any other automatic group, resolves exactly
one existing non-automatic external group, requires its public link to be
disabled, and records its immutable ID. App Store Connect
omits `hasAccessToAllBuilds` for non-automatic external groups, which Fastlane
2.232.2 exposes as `nil`; only that external omission is accepted, while a
literal `true` remains rejected as automatic. External export uses the same exact
manual profile and certificate mapping as internal export, explicitly leaves
`testFlightInternalTestingOnly` disabled, and requires the processed build to
report `APP_STORE_ELIGIBLE`. It rechecks that audience immediately before it
uploads with distribution and automatic notification disabled, associates the
exact processed build through the external ID, proves that the exact associated
group set is the approved automatic internal group plus the approved external
group, rejects individually assigned testers, and pins the external group's
exact tester relationship set using an approved count plus SHA-256 digest. It
requires every beta app localization to have a nonblank description and
feedback email, and requires one exact beta review detail with contact name,
email, phone, review notes, and an explicit demo-account flag (plus credentials
when the flag is true). These metadata checks run before upload and again before
submission. After submission, the lane refetches the exact build with its beta
review submission and accepts only Apple's pending, in-review, or approved
states. It rechecks both group memberships and the closed public-link state
after taking the durable notification claim and immediately before sending one
build-scoped tester notification.

After Apple processes an external upload, the lane resolves the exact App Store
Connect app/build resource IDs and creates a mode-400 upload-provenance receipt
at `TESTFLIGHT_UPLOAD_PROVENANCE_PATH`. The receipt contains only the exact app
and build IDs, bundle ID, version/build, source commit, IPA SHA-256, and UTC
recording time. The SHA-256 identifies the locally verified IPA supplied by the
same uninterrupted `pilot upload` invocation that returned successfully; Apple
does not independently attest that digest. Its mode-700 parent must be outside
the repository. Association
requires that receipt to match the same artifact bytes and source; deferred
notify-only runs require and revalidate it against the exact remote build. They
therefore cannot relabel an unrelated existing version/build as the reviewed
commit. The receipt is published from a uniquely named, fsynced same-directory
temporary inode with an atomic no-overwrite link, so a partial JSON document can
never occupy the final path. App Store Connect does not expose the uploaded
IPA's digest or an upload identity that can prove byte ownership after an
interrupted client. If the process stops after upload starts but before the
final receipt is published, do not automatically reconcile, associate, or
notify that remote build; inspect the incident and cut a new build number.
Before the non-idempotent notification request, the lane durably
creates a mode-600 `pending` claim containing the exact source commit, build, both
authorized group IDs, the associated group-ID set, and the approved external
tester count/digest plus the provenance-bound IPA SHA-256 at
`TESTFLIGHT_NOTIFICATION_RECEIPT_PATH` outside the repository. A second process
cannot create the same claim. Only a confirmed response atomically transitions
it to a mode-400 `complete` receipt with the returned notification ID. The
Expand All @@ -121,9 +160,13 @@ install software, change the version, or pick an ambiguous artifact.
If external beta review is still pending, the command fails closed after the
approved ID association. After approval, rerun with
`TESTFLIGHT_NOTIFY_ONLY=yes` and the printed immutable
`TESTFLIGHT_GROUP_ID`; this mode verifies the same clean commit, exact build,
group ID/name, exclusive group association, and notification state without
uploading again. Concurrent notification runs are prohibited. A `pending`
`TESTFLIGHT_GROUP_ID` plus the exact automatic internal group/tester IDs; this
mode first idempotently resumes association from the immutable finalized
provenance receipt, then verifies the same clean commit, exact build, closed
public-link state, exact two-group association, and notification state without
building or uploading again. This also safely resumes a process that stopped
after final provenance publication but before association.
Concurrent notification runs are prohibited. A `pending`
claim, interrupted run, or ambiguous App Store Connect response blocks every
retry until the release owner reconciles the exact build. If the owner can prove
the request never reached App Store Connect, they may archive the pending claim
Expand Down
30 changes: 24 additions & 6 deletions openhealth/.env.example
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ APPLE_TEAM_ID="XXXXXXXXXX"
APP_BUNDLE_ID="com.example.app"
LIVE_ACTIVITY_BUNDLE_ID="com.example.app.liveActivity"

# Internal TestFlight export is deliberately manual so a headless Xcode process
# TestFlight export is deliberately manual so a headless Xcode process
# cannot select a stale profile or depend on a signed-in Apple account. Supply
# the canonical UUID of the App Store profile for each exact target and the
# exact 40-character SHA-1 of the approved Apple Distribution certificate.
Expand All @@ -34,12 +34,29 @@ TESTFLIGHT_GROUP="External Testers"
# TESTFLIGHT_TESTER_ID="00000000-0000-0000-0000-000000000000"

# Internal mode requires the immutable ID of its approved automatic group.
# External mode uses the printed immutable ID only when safely resuming
# notification after App Store Connect beta review. Notify-only mode never
# uploads or re-associates the build.
# External mode requires the immutable ID from the start and also requires the
# exact automatic internal group and its sole tester because Apple gives that
# group every App Store-eligible build. The approved external group must have
# its public link disabled. Notify-only mode never uploads or re-associates.
# TESTFLIGHT_GROUP_ID="00000000-0000-0000-0000-000000000000"
# TESTFLIGHT_INTERNAL_GROUP="team"
# TESTFLIGHT_INTERNAL_GROUP_ID="00000000-0000-0000-0000-000000000000"
# TESTFLIGHT_INTERNAL_TESTER_ID="00000000-0000-0000-0000-000000000000"
# Pin the closed external group's exact membership without retaining tester
# identities in release logs or the repository: sorted relationship IDs joined
# with LF, then SHA-256. Recompute and explicitly approve any membership change.
# TESTFLIGHT_EXTERNAL_TESTER_COUNT="23"
# TESTFLIGHT_EXTERNAL_TESTER_IDS_SHA256="0000000000000000000000000000000000000000000000000000000000000000"
# TESTFLIGHT_NOTIFY_ONLY="yes"

# Immutable provenance for the exact uploaded IPA and processed App Store
# Connect build. Create its parent directory with mode 700 outside the
# repository. External upload mode creates this mode-400 file after processing;
# notify-only mode requires the same file and cannot infer provenance from a
# mutable version/build lookup. If upload is interrupted before this file is
# published, do not infer ownership from App Store Connect; cut a new build.
TESTFLIGHT_UPLOAD_PROVENANCE_PATH="/secure/openglucose/releases/0.1.0+17-upload.json"

# Durable one-shot claim and receipt for the exact build notification. Create
# its parent directory with mode 700 outside the repository. The lane writes a
# pending claim before the request and atomically marks it complete only after a
Expand All @@ -50,10 +67,11 @@ TESTFLIGHT_NOTIFICATION_RECEIPT_PATH="/secure/openglucose/releases/0.1.0+17-noti
RELEASE_VERSION="0.1.0+17"
RELEASE_COMMIT="full-40-character-git-commit"

# Release approval and exactly one audience gate are fail-closed.
# Release approval and audience gates are fail-closed. External mode requires
# both gates because Apple's automatic internal group receives the build too.
RELEASE_APPROVED="no"
DISTRIBUTE_EXTERNAL="no"
# Internal mode uses this gate instead and requires DISTRIBUTE_EXTERNAL != yes.
# Internal mode requires this gate with DISTRIBUTE_EXTERNAL=no.
# DISTRIBUTE_INTERNAL="no"

TESTFLIGHT_CHANGELOG="Describe the user-visible changes in this build."
Loading
0