Xcode Cloud
An archive workflow with HOTCODEPUSH_TOKEN as a secret, the post-clone script that installs Node.js, and the check that confirms the store build is registered.
The archive workflow on Xcode Cloud holds HOTCODEPUSH_TOKEN as a secret environment variable, and a post-clone script installs Node.js for the build step init wired. A check after the first archive confirms that the step registered the iOS store build.
Xcode Cloud builds only the iOS app. The Android store build runs elsewhere with its own token. Every merge's over-the-air (OTA) update ships from the CI on main: Azure DevOps, Bitbucket Pipelines, Capawesome Cloud, Gitea Actions, GitHub Actions or GitLab CI/CD. A release from Xcode Cloud would need a full archive on every merge and release each commit twice.
Ship every commit has the prerequisites and the three disciplines. This page carries the archive workflow and its script.
The secret to set
HOTCODEPUSH_TOKEN holds an API token from the console's API tokens page and lives in the Environment section of the archive workflow alone. Mark it "Keep value redacted" in Xcode or "Secret" in App Store Connect, so the logs show asterisks in its place. The workflow needs no signing key, since binary create signs nothing.
The archive workflow starts on Branch Changes for main or on Tag Changes, never on Pull Request Changes. A pull request can edit ci_post_clone.sh, and the scripts read every variable of their workflow. A pull request workflow with Build or Test actions needs no token, since those builds run resource-file write, which never fails without one.
The post-clone script
Xcode Cloud runs ci_post_clone.sh from the ci_scripts folder beside the Xcode project, right after the clone. The image carries Homebrew and CocoaPods but no Node.js, so the script installs one from the nodejs.org tarball.
#!/bin/sh
set -e
NODE_VERSION=24.22.1
NODE_ARCH=$([ "$(uname -m)" = arm64 ] && echo arm64 || echo x64)
NODE_DIR="$CI_DERIVED_DATA_PATH/node-v$NODE_VERSION-darwin-$NODE_ARCH"
mkdir -p "$CI_DERIVED_DATA_PATH"
curl -fsSLo "$CI_DERIVED_DATA_PATH/node.tar.xz" "https://nodejs.org/dist/v$NODE_VERSION/node-v$NODE_VERSION-darwin-$NODE_ARCH.tar.xz"
tar -xJf "$CI_DERIVED_DATA_PATH/node.tar.xz" -C "$CI_DERIVED_DATA_PATH"
export PATH="$NODE_DIR/bin:$PATH"
cd "$CI_PRIMARY_REPOSITORY_PATH"
echo "export NODE_BINARY=$NODE_DIR/bin/node" > ios/App/.xcode.env.local
echo "export CI=TRUE" >> ios/App/.xcode.env.local
npm ci
npm run build
npx cap sync iosMake the script executable with chmod +x ios/App/ci_scripts/ci_post_clone.sh before you commit it.
- A script's working directory is
ci_scripts, so it changes into$CI_PRIMARY_REPOSITORY_PATHbefore the install. - The build step does not inherit the script's environment, so the script writes
NODE_BINARYandCIintoios/App/.xcode.env.local, which the step sources before it runs. - A project on Swift Package Manager commits
ios/App/App.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved, and Xcode Cloud resolves the packages from that file. - Every action runs the scripts once, so a workflow with a Build and an Archive action installs Node.js and builds the web app twice.
React Native and Expo keep the scripts in ios/ci_scripts, write ios/.xcode.env.local instead and replace the last two lines:
| Framework | After npm ci |
|---|---|
| React Native | cd ios && pod install |
| Expo | npx expo prebuild --platform ios, then cd ios && pod install |
An Expo project commits the Xcode project the prebuild fills, since Apple requires a project continuously present in the repository. Cordova is not covered for the same reason: cordova platform add and prepare write its iOS project.
Check the first archive
After the first archive, run npx hotcodepush binary list: the iOS binary is listed with the version and build number TestFlight shows for that build. Apple documents the workflow's variables for the custom scripts alone, so the first archive shows whether the token reached the build step.
A missing token fails the archive, and the error catalog names its code. Any failed archive carries the CLI's exit code, and the error catalog names the fix.
Next steps
- Ship every commit has the release line the CI on
mainruns and the three disciplines it keeps. - GitHub Actions has the workflow that releases every merge. The other recipes share its shape.
- Compatibility explains the fingerprint the store build carries, read from the lockfile, never from
ios/.
Last updated on