MangoVM Engineering Guide

Manage iOS environments with xcconfig and audit every archive

Manage iOS environments with xcconfig and audit every archive

When the same iOS project connects to development, staging, and production services, the most dangerous failure is usually not a compilation error—it is successfully archiving the wrong environment. A development endpoint, verbose logging flag, or internal feature toggle can slip into a release build while the pipeline remains green. A safer approach is to separate public configuration into layers, standardize the build entry point, and treat archive inspection as a release step rather than relying on someone to select the right options in Xcode.

Define the boundary between configuration and secrets

xcconfig is appropriate for build settings that can safely be exposed with the client, such as the API base URL, Bundle ID suffix, log level, and default feature flags. It is not a secret vault. Any value included in the client build process may appear in Info.plist, resource files, compiler arguments, or the executable.

The rule is simple: if users should not know a value after obtaining the app package, do not let Xcode compile it into the App.

Server administration keys, signing-service credentials, and tokens with write access should remain on the server or in a controlled CI credential store. The client should receive only short-lived, low-privilege, revocable authorization results. Even when CI injects secrets through environment variables, the final artifact can still be analyzed.

Start by creating a configuration inventory. For each item, record whether it is public, where it comes from, and where it is expected to appear after archiving. This inventory—not dozens of values scattered across Build Settings—is what the team should review.

Create an auditable xcconfig hierarchy

Separate shared settings, environment-specific differences, and local overrides:

Config/
  Base.xcconfig
  Development.xcconfig
  Staging.xcconfig
  Production.xcconfig
  LocalOverrides.xcconfig.example

Base.xcconfig should contain only values shared by every environment. Each environment file includes it first and then overrides the small number of settings that differ:

#include "Base.xcconfig"

APP_ENVIRONMENT = staging
API_BASE_URL = https:/$()/staging-api.invalid
ENABLE_VERBOSE_LOGGING = YES
PRODUCT_BUNDLE_IDENTIFIER = com.example.product.staging

The $() prevents the double slash in the URL from being parsed as a comment. The example domain is intentionally non-resolvable documentation data; replace it with your own service endpoint in a real project.

Do not commit the local override file. Use an optional include to make the initial checkout easier:

#include? "LocalOverrides.xcconfig"

Local overrides should be limited to development convenience and must never become implicit inputs to a release archive. The production configuration must resolve completely when that file is absent. Map the Development, Staging, and Release Configurations to their corresponding files, and confirm that the Scheme used by CI is marked as Shared.

Pin the command-line build entry point

Archive jobs on a cloud Mac should not inherit state left behind by a previous graphical Xcode session. Pass the workspace, scheme, configuration, and output path explicitly:

set -euo pipefail

WORKSPACE="App.xcworkspace"
SCHEME="App"
CONFIGURATION="Release"
ARCHIVE_PATH="$PWD/build/App.xcarchive"

xcodebuild \
  -workspace "$WORKSPACE" \
  -scheme "$SCHEME" \
  -configuration "$CONFIGURATION" \
  -showBuildSettings > build-settings.txt

xcodebuild \
  -workspace "$WORKSPACE" \
  -scheme "$SCHEME" \
  -configuration "$CONFIGURATION" \
  -archivePath "$ARCHIVE_PATH" \
  clean archive

Verify resolved settings before archiving

Do not inspect only the source files. Check the settings that Xcode actually resolves. Extract the critical values from build-settings.txt and enforce hard assertions for production:

grep -E "APP_ENVIRONMENT|API_BASE_URL|PRODUCT_BUNDLE_IDENTIFIER|ENABLE_VERBOSE_LOGGING" \
  build-settings.txt

grep -q "APP_ENVIRONMENT = production" build-settings.txt
grep -q "ENABLE_VERBOSE_LOGGING = NO" build-settings.txt

If the project has multiple Targets, inspect each Target separately because extensions, test bundles, and the main App may inherit different Base Configurations. The script should also print the current Git commit, Xcode version, and selected Scheme so the exact context can be reconstructed when results differ.

Scan the archive that will actually be delivered

Correct Build Settings do not guarantee a correct artifact. Run Script phases, generated code, or resource-copying steps can still introduce test configuration into the archive. After archiving, inspect at least the main App’s property list, embedded resources, and executable strings.

APP_PATH="$(find build/App.xcarchive/Products/Applications -maxdepth 1 -name '*.app' -print -quit)"
test -n "$APP_PATH"

plutil -p "$APP_PATH/Info.plist"

if grep -RInaE "staging-api|debug-token|INTERNAL_ONLY" "$APP_PATH"; then
  echo "forbidden marker found in archive" >&2
  exit 1
fi

EXECUTABLE_NAME=$(/usr/libexec/PlistBuddy -c "Print :CFBundleExecutable" "$APP_PATH/Info.plist")
if strings "$APP_PATH/$EXECUTABLE_NAME" | grep -E "staging-api|debug-token|INTERNAL_ONLY"; then
  echo "forbidden marker found in executable" >&2
  exit 1
fi

Use team-defined markers as scan terms; never place real secrets directly in scripts or logs. Maintain two short lists—one for allowed values and one for forbidden values—to avoid false positives from ordinary words. Scan results should record only the file path and rule name, without echoing the full sensitive match.

Inspection target What to verify Action on failure
Final Build Settings Environment name, Bundle ID, logging flag Stop the archive immediately
Info.plist Service endpoint, URL Scheme, environment label Block export
App resource directory Debug configuration, test data, temporary files Remove the source and rebuild
Executable Internal endpoint patterns, token markers Identify the generation step and rotate related credentials

Address common sources of drift and false assumptions

The most common source of drift is a Scheme that exists locally but has not been shared. If the corresponding xcshareddata is missing from the repository, CI may be unable to find the Scheme, or an engineer may temporarily switch to a different entry point. Another common problem is branching on the environment name inside scripts without treating the Configuration as the single source of truth. Over time, this creates two independent sets of environment-selection logic.

Another false assumption is that a variable becomes safe once it is obfuscated, split apart, or embedded in a binary. There is no reliable way to hide a long-lived secret in a client. If one is found in a historical archive, revoke or rotate the credential first, then fix the build configuration. Merely deleting the string from the current branch does not eliminate the risk from builds that have already been distributed.

When running this process on MangoVM fixed physical nodes, configure the Runner to use a clean working directory. Do not reuse DerivedData, temporary configuration, or export directories generated by the previous job. Restrict cleanup to the current workspace so concurrent jobs cannot delete one another’s files.

Turn checks into merge and release gates

The workflow can ultimately be split into two stages. During merge requests, resolve the settings and validate the configuration-file structure. During releases, perform a full archive and artifact scan. The first stage provides fast feedback; the second verifies the artifact that will actually be delivered. Every failed environment assertion must return a nonzero status rather than merely writing a warning to the log.

Use this checklist before shipping:

Once configuration sources, resolved settings, and final artifacts provide three layers of evidence, environment selection no longer depends on individual habits. Release failures are caught before the archive leaves the pipeline, rather than being discovered by users.

Frequently asked questions

Should API secrets be stored in xcconfig files?

No. Build settings can be copied into Info.plist files, compiler flags, resources, or the executable. An app archive must be treated as public, so privileged credentials belong on a server rather than inside the client build.

Why does CI archive a different environment from a local Mac?

Typical causes are an unshared scheme, a mismatched configuration mapping, or variables that exist only in an interactive shell. Pass the workspace, scheme, and configuration explicitly, then record xcodebuild -showBuildSettings for comparison.

Dedicated Physical Node

Configure a Dedicated Cloud Mac for Your Development Pipeline

MangoVM offers M4 and M4 Pro Apple Silicon physical nodes. Choose your term, region, and storage add-ons to review the complete order details.

Choose Configuration and Order