iOS Build and App Store Setup
iOS Build and App Store Setup
FrogCityFeast uses Windows for normal development and a GitHub-hosted macOS runner only for Apple-specific Godot export, Xcode compilation, signing, and submission.
For the reusable setup order, failure modes, API-role requirements, and lessons
to apply before creating another app, read
apple-app-publishing-runbook.md.
Production distribution decision
The September 1, 2026 production decision selects a normal public App Store release. TestFlight is unavailable to the target device because its Apple Account is under 13. Do not falsify the account age, bypass the restriction, or use the existing internal TestFlight path as the target-device installation plan.
The repository includes a separate manual
iOS App Store candidate upload
workflow. It uses the protected app-store environment and an export mode that
omits testFlightInternalTestingOnly. A second protected approval lets the
signing job consume the existing credentials from the testflight environment
without copying or revealing them. The existing TestFlight workflow and
internal group remain historical infrastructure; they are not the selected
distribution route.
The manual
App Store candidate inspection
workflow uses the same two approvals and API credentials for read-only
inspection after upload. It waits for the exact build to finish processing and
reports build validity, export-compliance state, selected-build status,
13-inch iPad screenshot completeness, and App Review contact completeness
without printing contact values or selecting, submitting, or releasing a
build.
The separately authorized
App Store submission preparation
workflow runs on Windows, checks out the exact uploaded candidate source,
regenerates and validates the approved seven-image package, uploads screenshots
directly to Apple, creates or updates the App Review detail from protected
contact secrets, and selects only the exact processed build. Generated images
remain runner-local and are always removed. The workflow cannot create a
review submission or release request.
The separately authorized
App Store review submission
workflow rechecks the exact version, selected build, screenshots, review
details, processing state, export compliance, and manual release type. It
creates or safely resumes one iOS review submission, adds only version 0.1.0,
and submits it to App Review. It cannot request release, create a tag, publish
a GitHub release, or change the version from manual release.
The distribution decision alone did not authorize signing, upload, submission, publication, release, or tagging. Signing, upload, and App Review submission were performed only after their later explicit authorizations. Publication, manual release, and tag or GitHub release creation remain unauthorized until Apple approves the version, the remaining release checklist is complete, and the repository owner gives separate explicit release authorization.
Unsigned pipeline verification
The credential-free pipeline is verified through
iOS unsigned smoke build.
Manual run
33327784555
completed successfully on August 30, 2026 for audio commit
73cfb5ef1cbc8d3d5e9eb71dbec44a4455d8fd76:
- the pinned Godot 4.7.2 editor and export templates installed on
macos-26; - the Xcode 26.6 and iOS 26.5 preflight passed;
- Godot generated
FrogCityFeast.xcodeprojwith theFrogCityFeastscheme; - Xcode compiled the Release configuration for a generic arm64 iOS device with signing disabled; and
- no certificate, provisioning profile, App Store credential, or build artifact was used or uploaded.
The normal Windows/Linux project check also runs
tests/ios_pipeline_smoke.gd. This deterministic regression check verifies the
manual-only trigger, read-only permissions, pinned action commits and
toolchain, absence of secrets, generated project-only arm64 preset, temporary
export_presets.cfg cleanup, and the explicit Xcode signing overrides. It does
not replace the macOS integration run.
Godot CI run
33327743256
also passed on that exact commit. The successful unsigned run includes the
generated-project validator, iPad-only target, explicit icon, privacy
declarations, warning sanitation, original audio files,
default_bus_layout.tres, and the audio autoload.
The cited runs cover the audio commit only. Later gameplay commits require their own Godot CI result, and remain outside the unsigned iOS integration run until that manual workflow is separately authorized. Do not approve a signed release until both checks pass on the intended release commit.
The successful build retains non-fatal Godot/Xcode warnings for a legacy boot splash property, empty camera/photo-library/microphone purpose strings emitted by the generated project, a generated header pragma, and skipped App Intents metadata. The shared export script now removes only the three empty, unused purpose-string keys and fails if any of them becomes non-empty. It also validates the generated AppIcon catalog, bundle/version build settings, encryption declaration, and Godot privacy manifest before either iOS workflow continues.
The remaining warnings require no repository workaround:
| Warning | Release disposition |
|---|---|
application/boot_splash/fullsize property not found |
Godot 4.7.2’s iOS exporter still queries this legacy property. The project does not define it; adding an obsolete setting would hide rather than fix the exporter warning. |
| Empty camera, microphone, and photo-library purpose strings | Removed from the generated project because the game uses none of these capabilities. Do not add inaccurate purpose text. |
dummy.h has #pragma once in the main file |
Generated Godot template warning with no effect on the compiled application. Do not patch generated engine files. |
| App Intents metadata extraction skipped | Expected because the game does not link AppIntents.framework or provide App Intents. |
Godot 4.7.2 generates and embeds PrivacyInfo.xcprivacy. The export preset
declares file timestamp access only inside the app container, system boot time
only for on-device elapsed-time measurement, and disk-space access only for
writing or deleting files. Tracking and data collection are disabled. The
generated-project validator fails if those declarations drift.
Workflow design
| Workflow | Trigger | Credentials | Purpose |
|---|---|---|---|
Godot CI |
Push, pull request, manual | None | Import, start, and run deterministic project checks on Linux |
iOS unsigned smoke build |
Manual | None | Export through Godot and compile an unsigned generic iOS device target |
iOS Ad Hoc registered-device validation |
Manual from main with explicit confirmation |
Protected ad-hoc authorization followed by protected testflight certificate, three device secrets, and provisioning-only Admin API key |
Automatically reconcile the exact devices/profile, then sign and validate without retaining or publishing the IPA |
iOS App Store candidate upload |
Manual from main with explicit confirmation |
Protected app-store authorization followed by protected testflight signing credentials |
Archive, sign, and upload a normal App Store candidate without submitting it for review |
iOS TestFlight release |
Manual from main or a v* tag |
Protected testflight environment |
Historical internal-only upload path; not usable by the target under-13 account |
The smoke workflow uses the synthetic Team ID 0000000000 only because Godot
requires a non-empty Team ID before generating an Xcode project. Xcode signing
is explicitly disabled, so this value never identifies or accesses an Apple
account.
All iOS workflows use the stable macos-26 arm64 runner, Xcode 26.6, and the
iOS 26.5 SDK recorded in tools/toolchain.json. They download the Godot macOS
editor and export templates from the official Godot release, then verify the
SHA-512 checksums before use.
The iOS preset explicitly targets iPad only, matching the documented product scope and 4:3 presentation. Supporting iPhone later requires a separately reviewed layout, device-testing, and export-preset change.
The Ad Hoc path is documented in
ios-ad-hoc-testing.md. Its three device secrets and
separate provisioning-only Admin key are configured. The protected workflow
now registers missing devices and creates or reuses the exact matching profile
through Apple’s API; no manual profile download is needed. Its default mode
retains no signed artifact. The separately confirmed private OTA mode uploads
only after final IPA signature and embedded-profile validation, using the
private Azure storage path documented in ios-ad-hoc-testing.md. Protected run
33703681354
completed device/profile reconciliation, signing, release-testing export,
embedded-profile validation, and verified cleanup for build 33703681354.1.
Protected run
33768105238
then completed the separately authorized private OTA upload for build
33768105238.1, confirmed anonymous access remained denied, retained no
GitHub artifact, and verified cleanup. This private registered-device build
does not submit, approve, or release the normal App Store version.
Apple account prerequisites
The selected release identity is:
| Field | Selected value |
|---|---|
| Bundle identifier | com.chdafni.frogcityfeast |
| App Store Connect name | Frog City Feast |
| SKU | FROGCITYFEAST-IOS-001 |
| Primary language | English (U.S.) |
| Apple Developer Team ID | CV7JQ487YU |
| Selected distribution | Normal public App Store release |
| Non-exempt encryption | No; ITSAppUsesNonExemptEncryption is false |
The bundle identifier uses reverse-DNS syntax but does not require ownership of a matching internet domain. Treat it as permanent after creating the App Store Connect record. The Godot project and generated iPad application display name are also Frog City Feast.
Current prerequisite status:
| Prerequisite | Status |
|---|---|
| Explicit App ID | Created for com.chdafni.frogcityfeast. Apple automatically enables the immutable In-App Purchase feature for this Universal App ID; the game has no StoreKit integration, products, or purchase UI. No optional capability was requested. |
| App Store Connect app record | Created for Frog City Feast with primary locale en-US, bundle ID com.chdafni.frogcityfeast, and SKU FROGCITYFEAST-IOS-001. |
| Apple Distribution certificate | Created and valid through August 30, 2027. Its private key and randomly generated .p12 password exist only in the protected GitHub environment secret set. |
| App Store provisioning profile | Created for the exact App ID and certificate, validated, and valid through August 30, 2027. |
| App Store Connect API keys | An App Manager-role team key remains configured for metadata and upload work. A separate Admin Team API key is configured only for protected device/profile provisioning and is retained for future provisioning per the repository owner’s request. Both use the existing issuer; their Key IDs and private-key secrets remain distinct. Neither private key is committed or retained on a GitHub runner. |
| Ad Hoc registered-device path | Validated successfully in protected run 33703681354 for the exact three configured devices and matching profile. The signed IPA was validated, not uploaded or published, and removed during verified cleanup. |
| Internal TestFlight group | Frog City Feast Internal exists as an internal group with the sole App Store Connect user added. Automatic access to all builds is disabled and no public link is enabled. The tester remains NOT_INVITED until a build is assigned. |
| Apple agreements and compliance | The account holder confirmed MFA, current legal agreements, EU DSA non-trader status, and publication of the no-data App Privacy response. Protected run 33825382288 confirmed that TRADER_STATUS_NOT_PROVIDED cleared. After the final App Privacy Publish confirmation, submission run 33828163019 succeeded. |
The signing identity, App Store app record, and exact replacement build
33770597608.1 exist. Protected run
33823901657
confirmed the build is valid and selected, the seven final screenshots and
App Review detail are complete, content rights and age rating match, free
pricing is active, China mainland is unavailable, and release remains manual.
The Account Holder completed the EU DSA non-trader declaration and published
the no-data App Privacy response. Protected submission run
33828163019
recreated the deleted review draft, added exact version 0.1.0, and submitted
build 33770597608.1. Apple returned WAITING_FOR_REVIEW; release remains
manual and no release was requested. Physical A16 acceptance remains
separately tracked. Complete the remaining review and release gates in
app-store-release-checklist.md. Build
selection, App Review submission, and release remain separate App Store
Connect actions regardless of the API key’s role.
The separate protected App Store metadata sync workflow can apply the
reviewed API-supported listing fields without uploading a build. Apple rejects
primary-locale and content-rights updates on the existing app record even
though those attributes remain in the current OpenAPI update schema, so those
values require direct confirmation. Run
33616496541
proved that authentication and read access do not establish metadata write
permission. The protected key was replaced with App Manager access without
broadening it to Admin. Runs
33648261223
and
33653860478
exposed and partially applied the initial-version path. Run
33661855538
then completed the metadata sync, including version localization and the age
rating. The owner directly saved content rights, free pricing, and every
storefront except China mainland. The protected inspector now verifies those
resources, screenshots, and App Review contact. The EU DSA declaration is now
complete, and the no-data App Privacy response is published. Apple exposes no
supported API for independently inspecting the questionnaire’s publication
state, so the successful review submission is the authoritative confirmation.
Every provisioning profile must use the same Team ID and exact bundle
identifier configured in GitHub. The normal App Store path rejects development,
Ad Hoc, enterprise, expired, wildcard, or mismatched profiles. The separate Ad
Hoc path requires IOS_APP_ADHOC, exactly three protected devices, and no
enterprise/debug entitlement. Both verify that the profile includes the
supplied, unexpired Apple Distribution certificate and that the certificate
Team ID is CV7JQ487YU.
Protected GitHub environments
The selected public workflow first enters a dedicated authorization environment
named app-store. It allows only branch main, requires CHDAFNI-MSFT as
reviewer, and keeps self-review prevention disabled while the repository has
only one release operator. Administrator bypass is disabled. The workflow is
manual-only and requires its confirm_upload input.
After that approval succeeds, the signing job enters the protected
testflight environment and requires its separate approval before credentials
become available. This consumes the existing validated secrets in place rather
than copying or exposing them. The job repeats the main and confirmation
conditions, administrator bypass is disabled for this environment too, and the
job still selects the normal app-store export mode. The historical TestFlight
workflow remains unavailable to the target under-13 account.
The registered-device workflow uses the same two-stage boundary. Its ad-hoc
authorization environment allows only main, requires CHDAFNI-MSFT, has
administrator bypass disabled, and stores no variables or secrets. Only after
that approval does the job enter testflight for the certificate, three
protected UDIDs, and provisioning-only Admin key. The Admin credentials are
passed only to the automatic device/profile provisioning step and are not
available to signing, archiving, OTA upload, or metadata mutation steps. The
OTA step receives only its dedicated revocable Azure policies when the manual
publish_private_ota input is enabled and confirm_private_ota exactly equals
UPLOAD_PRIVATE_OTA.
Add these environment variables:
| Variable | Value |
|---|---|
APPLE_TEAM_ID |
CV7JQ487YU |
IOS_BUNDLE_ID |
com.chdafni.frogcityfeast |
AZURE_OTA_STORAGE_ACCOUNT |
stmobileota2041340e8c |
AZURE_OTA_CONTAINER |
ios-delivery |
Configure APPLE_TEAM_ID and IOS_BUNDLE_ID in app-store as reviewable
identifiers and keep the same values in testflight. Configure the two
AZURE_OTA_* variables only in testflight, where the opt-in private upload
step reads them. Never use repository-level variables or secrets for these
workflows.
The normal App Store signing and App Manager values remain in the protected
historical testflight environment and passed certificate, profile, and
authenticated App Store Connect validation. The approved route leaves them
there and keeps app-store free of signing secrets. The separate Admin Team
key is retained only for future protected provisioning as requested by the
owner. No local copy of the certificate private key, .p12, password, CSR,
certificate, profile, or API private key is retained by the repository or
workflow runner.
The signed job reads these existing testflight environment secrets:
| Secret | Content |
|---|---|
APPLE_CERTIFICATE_BASE64 |
Base64-encoded Apple Distribution .p12 |
APPLE_CERTIFICATE_PASSWORD |
Password used when exporting the .p12 |
APPLE_PROVISIONING_PROFILE_BASE64 |
Base64-encoded App Store .mobileprovision |
APP_STORE_CONNECT_KEY_ID |
App Store Connect API Key ID |
APP_STORE_CONNECT_ISSUER_ID |
App Store Connect API Issuer ID |
APP_STORE_CONNECT_PRIVATE_KEY_BASE64 |
Base64-encoded App Store Connect .p8 |
Registered-device testing uses these configured separate secrets without replacing the App Store profile or App Manager credentials:
| Secret | Content |
|---|---|
IOS_AD_HOC_DEVICE_UDID_1 through _3 |
Three independently masked protected iPad UDIDs |
APPLE_PROVISIONING_KEY_ID |
Key ID for the provisioning-only Admin Team API key |
APPLE_PROVISIONING_PRIVATE_KEY_BASE64 |
Base64-encoded private key for that provisioning-only Admin key |
AZURE_OTA_UPLOAD_SAS |
Revocable HTTPS-only workflow-upload stored-policy SAS |
AZURE_OTA_INSTALL_SAS |
Revocable HTTPS-only read-only device-install stored-policy SAS |
The existing APP_STORE_CONNECT_ISSUER_ID, distribution certificate, and
certificate password are reused by the provisioning step. The device and Admin
key secrets are configured. No Ad Hoc profile secret is required: the workflow
registers only missing exact devices, creates or reuses the deterministic
exact-membership profile, validates its API content, and hands its protected
RUNNER_TEMP path to signing.
On macOS, encode each binary or key file without adding line wrapping:
base64 -i Distribution.p12 | pbcopy
base64 -i AuthKey_KEYID.p8 | pbcopy
On Windows PowerShell:
[Convert]::ToBase64String(
[IO.File]::ReadAllBytes("Distribution.p12")
) | Set-Clipboard
Repeat the PowerShell command for an API private key. Keep all source files outside the repository, enter the encoded values directly in GitHub, clear the clipboard, and delete local copies when they are no longer needed. The retained provisioning key exists only as a protected secret; no manual Ad Hoc profile download is needed. Never place certificate, profile, private-key, or password files inside the repository.
Authorized public release sequence
- Complete
app-store-release-checklist.md, including live support/privacy URLs, accurate metadata and rating answers, final screenshots, exact-commit checks, and physical A16 iPad acceptance. - Run
Godot CIsuccessfully on the intended commit. - Run
iOS unsigned smoke buildon that exact commit. Do not proceed until generated-project validation and the unsigned Xcode compile both pass. - Obtain explicit upload authorization for one exact commit and marketing
version. The current workflow is fail-closed to the authorized version
0.1.0; changing it requires a separately reviewed commit and authorization. - Confirm the
app-storeandtestflightbranch restrictions, reviewers, non-secret variables, and existingtestflightsecret names. - Manually run
iOS App Store candidate uploadfrommain, enter0.1.0, and enableconfirm_upload. - Approve the protected
app-storeauthorization job, then separately approve thetestflightsigning job. Credentials become available only after the second approval. - Wait for App Store Connect processing and inspect the candidate. The workflow does not select the build for a version, submit it for App Review, or release it.
- Obtain separate explicit submission authorization before selecting the processed build and submitting the complete version to App Review.
- Select Manually release this version and obtain separate explicit release authorization before making an approved version public.
Both upload workflows combine the repository-wide github.run_id and
github.run_attempt for CFBundleVersion. Run IDs are unique across workflows,
and a rerun receives a new attempt component, so the retained TestFlight path
cannot collide with the public candidate path.
Historical TestFlight path
The target under-13 Apple Account cannot use TestFlight. Do not change the
account age, route around Apple’s restriction, or treat another account’s
internal install as acceptance for the target player. The retained
iOS TestFlight release workflow explicitly selects internal-testflight;
its generated export options include testFlightInternalTestingOnly, so those
builds cannot be promoted to App Review. The public workflow selects
app-store, whose generated export options omit that key.
Signing and cleanup behavior
The release job:
- Decodes signing files only into
$RUNNER_TEMP. - Validates the provisioning profile Team ID and application identifier.
- Rejects expired or non-App-Store profiles and verifies that the supplied Apple Distribution certificate belongs to both the Team ID and profile.
- Creates a random-password temporary keychain.
- Imports only the Apple Distribution identity.
- Installs the profile into both current and legacy Xcode profile locations.
- Exports and validates the Godot-generated Xcode project.
- Archives with manual signing and no permission to create or update signing assets in the Apple portal.
- Uploads through
xcodebuild -exportArchiveusing the App Store Connect API key. Internal mode adds the TestFlight-only key; public mode omits it and creates a normal App Store candidate. - Deletes the temporary keychain, profile copies, private key, archive, and DerivedData, export output, and build logs even when an earlier step fails.
No signed IPA, Xcode archive, provisioning profile, or signing key is uploaded as a GitHub artifact. Workflows have read-only repository permissions and checkout does not persist Git credentials. Uploading a public candidate does not select it for an App Store version, submit it for review, or publish it. Those remain separately authorized manual actions.
Maintenance
The runner label, Xcode version, Xcode developer directory, and iOS SDK are pinned because GitHub runner defaults change over time. Before updating them:
- Confirm the target image is stable rather than preview.
- Verify the Xcode and SDK versions in the official runner image manifest.
- Update
tools/toolchain.json, all three iOS workflows, and this document in one change. - Run the unsigned smoke workflow before allowing any signed upload.
The macOS release path cannot be fully exercised from Windows. Local validation covers project import and startup plus deterministic workflow, manifest, preset, secret, and signing-override checks. The manual smoke workflow is the authoritative integration test for Godot export and Xcode compilation. Run it again after committing changes that affect project resources, scenes, scripts, icons, or export configuration.