feat(ios): add native build and TestFlight automation

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
Dennis V
2026-07-14 06:16:36 +00:00
parent d546c41e22
commit 791588647b
9 changed files with 378 additions and 216 deletions

137
.github/workflows/ios-ci.yml vendored Normal file
View File

@@ -0,0 +1,137 @@
# iOS build gate for pull requests and main.
#
# Requires NO Apple/EAS secrets: it validates the Expo config, exports the iOS JS
# bundle, generates the native project, installs CocoaPods, and compiles an
# UNSIGNED iPhone Simulator target with xcodebuild. Signing/TestFlight lives in
# publish-app-store.yml.
#
# Cheap platform-neutral checks (typecheck + unit tests) run first on Linux and
# gate the costly macOS native build.
name: iOS CI
on:
pull_request:
branches: [main]
paths-ignore:
- "**/*.md"
- "docs/**"
- "docs-site/**"
- "distribution/**"
push:
branches: [main]
paths-ignore:
- "**/*.md"
- "docs/**"
- "docs-site/**"
- "distribution/**"
# Cancel superseded runs for the same ref (e.g. new push to an open PR).
concurrency:
group: ios-ci-${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
permissions:
contents: read
jobs:
test:
name: Typecheck and unit tests
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- uses: actions/setup-node@v6
with:
# Node >= 23.6 runs the TypeScript test files natively (type-stripping),
# matching the local toolchain. No build step or extra deps required.
node-version: 24
cache: npm
- name: Install dependencies (deterministic)
run: npm ci --legacy-peer-deps
- name: Typecheck
run: npm run typecheck
- name: Unit tests
run: npm test
ios-build:
name: Unsigned iOS Simulator build
needs: test
# macos-15 ships Xcode 16.x, which React Native 0.81 / Expo SDK 54 require.
runs-on: macos-15
timeout-minutes: 45
env:
# No source-map upload from CI (no Sentry auth token here); keep the build hermetic.
SENTRY_DISABLE_AUTO_UPLOAD: "true"
steps:
- uses: actions/checkout@v6
- uses: actions/setup-node@v6
with:
node-version: 20
cache: npm
- name: Install dependencies (deterministic)
run: npm ci --legacy-peer-deps
- name: Validate Expo config and plugin resolution
run: npx expo config --type introspect --json > expo-config.introspect.json
- name: Export iOS JavaScript bundle
run: npx expo export --platform ios --output-dir dist
- name: Prebuild native iOS project
run: npx expo prebuild --platform ios --no-install
- name: Install CocoaPods dependencies
working-directory: ios
run: pod install
- name: Resolve Xcode workspace and scheme
id: xc
run: |
set -euo pipefail
shopt -s nullglob
workspaces=(ios/*.xcworkspace)
workspace="${workspaces[0]:-}"
if [ -z "$workspace" ]; then
echo "::error::No .xcworkspace was generated by expo prebuild."
exit 1
fi
scheme=$(xcodebuild -workspace "$workspace" -list -json | node -e "
const d = JSON.parse(require('fs').readFileSync(0, 'utf8'));
const all = (d.workspace && d.workspace.schemes) || [];
const app = all.filter((s) => s !== 'Pods' && !s.startsWith('Pods-'));
if (app.length === 0) { console.error('No application scheme found in workspace'); process.exit(1); }
process.stdout.write(app[0]);
")
echo "workspace=$workspace" >> "$GITHUB_OUTPUT"
echo "scheme=$scheme" >> "$GITHUB_OUTPUT"
echo "Using workspace='$workspace' scheme='$scheme'"
- name: Build unsigned iPhone Simulator app
run: |
set -euo pipefail
NSUnbufferedIO=YES xcodebuild \
-workspace "${{ steps.xc.outputs.workspace }}" \
-scheme "${{ steps.xc.outputs.scheme }}" \
-configuration Debug \
-sdk iphonesimulator \
-destination 'generic/platform=iOS Simulator' \
-derivedDataPath ios/build \
CODE_SIGNING_ALLOWED=NO \
CODE_SIGNING_REQUIRED=NO \
CODE_SIGN_IDENTITY="" \
build 2>&1 | tee xcodebuild.log
- name: Upload xcodebuild log
if: always()
uses: actions/upload-artifact@v7
with:
name: ios-xcodebuild-log
path: xcodebuild.log
retention-days: 14
if-no-files-found: ignore

View File

@@ -1,45 +1,59 @@
# STATUS: Validated structure — awaiting Apple Developer Program enrollment approval.
# Once enrollment is approved, complete these steps and this workflow is production-ready:
# Publish OpenCode for iOS to TestFlight with EAS Build + EAS Submit.
#
# REMAINING GAPS (must complete before first run):
# 1. Update eas.json: replace REPLACE_WITH_APP_STORE_CONNECT_APP_ID and REPLACE_WITH_APPLE_TEAM_ID
# (see eas.json.README.md for exact click paths in App Store Connect)
# 2. Add GitHub secrets (Settings > Secrets and variables > Actions):
# EAS_TOKEN Expo access token (expo.dev > Account > Access Tokens)
# APPLE_APP_STORE_CONNECT_API_KEY_ID Key ID from App Store Connect > Users & Access > Integrations > App Store Connect API
# APPLE_APP_STORE_CONNECT_ISSUER_ID Issuer ID from same page
# APPLE_APP_STORE_CONNECT_API_KEY base64-encoded .p8 file (download at key creation — one time only)
# 3. Run `eas login` locally and `eas build:configure` on first run to let EAS set up signing
# 4. Manually upload first build to App Store Connect (required once to create the app record)
# This workflow FAILS FAST (non-zero exit) instead of "succeeding by skipping":
# a release with missing credentials or unfilled identifiers is a hard error, so a
# green run always means a real build was produced and submitted.
#
# OPTIONAL secrets (crash reporting):
# EXPO_PUBLIC_SENTRY_DSN SENTRY_AUTH_TOKEN SENTRY_ORG SENTRY_PROJECT
# ── HUMAN GATE (one-time, after Apple Developer Program enrollment) ──────────────
# Complete ALL of the following before releasing. Do NOT invent any of these IDs.
#
# Build strategy: EAS Build (Expo Application Services)
# - No Mac runner needed; Expo hosts macOS workers with managed certificates.
# - Cost: free tier (30 builds/month); upgrade to $19/month for unlimited/priority queue.
# - See distribution/ios-enrollment-runbook.md for full enrollment steps.
# - See eas.json.README.md for placeholder fill-in instructions.
# - Alternative (self-hosted Mac runner): see commented section at bottom of this file.
# 1. Fill and commit the eas.json placeholders (see eas.json.README.md for click paths):
# submit.production.ios.ascAppId REPLACE_WITH_APP_STORE_CONNECT_APP_ID → numeric App Store Connect App ID
# submit.production.ios.appleTeamId REPLACE_WITH_APPLE_TEAM_ID → 10-char Apple Team ID
#
# 2. Add GitHub Actions secrets (Settings → Secrets and variables → Actions):
# EXPO_TOKEN Expo access token (expo.dev → Account settings → Access tokens)
# APPLE_APP_STORE_CONNECT_API_KEY_ID ASC API Key ID (App Store Connect → Users and Access → Integrations → App Store Connect API)
# APPLE_APP_STORE_CONNECT_ISSUER_ID ASC API Issuer ID (same page)
# APPLE_APP_STORE_CONNECT_API_KEY base64 of the .p8 key file: `base64 -i AuthKey_XXXX.p8` (downloadable once)
#
# 3. Bootstrap iOS signing credentials on EAS once (creates the distribution cert +
# provisioning profile so CI never needs to prompt):
# eas login && eas build --platform ios --profile production
#
# Optional crash reporting belongs in the EAS `production` environment because the
# iOS bundle is built on a remote EAS worker. Configure EXPO_PUBLIC_SENTRY_DSN,
# SENTRY_AUTH_TOKEN, SENTRY_ORG, and SENTRY_PROJECT in Expo before releasing.
#
# Build number is managed remotely by EAS (eas.json: cli.appVersionSource=remote,
# build.production.ios.autoIncrement=buildNumber). No app.json mutation happens here.
name: Publish to App Store (TestFlight)
on:
# One release ⇒ one build. Triggering only on `release: published` avoids the
# duplicate build that a combined release+tag trigger would create.
release:
types: [published]
push:
tags: ["v*"]
workflow_dispatch:
jobs:
publish-ios:
runs-on: ubuntu-latest
env:
EXPO_PUBLIC_SENTRY_DSN: ${{ secrets.EXPO_PUBLIC_SENTRY_DSN }}
SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
SENTRY_ORG: ${{ secrets.SENTRY_ORG }}
SENTRY_PROJECT: ${{ secrets.SENTRY_PROJECT }}
# Serialize runs per release so a re-trigger cannot start a duplicate concurrent
# build/submit. cancel-in-progress:false never kills an in-flight submission.
concurrency:
group: publish-app-store-${{ github.event.release.tag_name || github.ref_name }}
cancel-in-progress: false
permissions:
contents: read
jobs:
testflight:
name: EAS Build and submit to TestFlight
runs-on: ubuntu-latest
timeout-minutes: 90
env:
EAS_CLI_VERSION: "21.0.0"
EXPO_TOKEN: ${{ secrets.EXPO_TOKEN }}
steps:
- uses: actions/checkout@v6
@@ -48,144 +62,148 @@ jobs:
node-version: 20
cache: npm
- name: Check Apple prerequisites
id: check-apple
run: |
if [[ -n "${{ secrets.EAS_TOKEN }}" ]]; then
echo "proceed=true" >> "$GITHUB_OUTPUT"
else
echo "proceed=false" >> "$GITHUB_OUTPUT"
echo "::warning::Apple Developer enrollment pending — EAS_TOKEN not set. Skipping iOS build."
fi
# Install EAS CLI globally. Pin to a recent stable version.
- name: Install EAS CLI
if: steps.check-apple.outputs.proceed == 'true'
run: npm install -g eas-cli@13
- name: Install dependencies
if: steps.check-apple.outputs.proceed == 'true'
run: npm install --legacy-peer-deps
# Bump ios.buildNumber to match github.run_number (monotonically increasing).
# App Store Connect rejects duplicate build numbers for the same version string.
- name: Bump ios.buildNumber in app.json
if: steps.check-apple.outputs.proceed == 'true'
run: |
node -e "
const f = 'app.json';
const j = require('./' + f);
j.expo.ios = j.expo.ios || {};
j.expo.ios.buildNumber = String(${{ github.run_number }});
require('fs').writeFileSync(f, JSON.stringify(j, null, 2) + '\n');
"
echo "buildNumber now: $(node -p "require('./app.json').expo.ios.buildNumber")"
# EAS Build: builds the IPA in Expo's cloud (macOS workers managed by Expo).
# --non-interactive: no prompts, suitable for CI.
# --platform ios: iOS only (Android is handled by publish-play-store.yml).
# --profile production: uses the "production" profile in eas.json (created below if missing).
- name: Build IPA via EAS
if: steps.check-apple.outputs.proceed == 'true'
- name: Preflight — verify credentials and identifiers (fail fast)
env:
EXPO_TOKEN: ${{ secrets.EAS_TOKEN }}
APPLE_APP_STORE_CONNECT_API_KEY_ID: ${{ secrets.APPLE_APP_STORE_CONNECT_API_KEY_ID }}
APPLE_APP_STORE_CONNECT_ISSUER_ID: ${{ secrets.APPLE_APP_STORE_CONNECT_ISSUER_ID }}
APPLE_APP_STORE_CONNECT_API_KEY: ${{ secrets.APPLE_APP_STORE_CONNECT_API_KEY }}
ASC_KEY_ID: ${{ secrets.APPLE_APP_STORE_CONNECT_API_KEY_ID }}
ASC_ISSUER_ID: ${{ secrets.APPLE_APP_STORE_CONNECT_ISSUER_ID }}
ASC_KEY_B64: ${{ secrets.APPLE_APP_STORE_CONNECT_API_KEY }}
run: |
set -euo pipefail
fail=0
need() {
if [ -z "${2:-}" ]; then
echo "::error::Missing required secret: $1"
fail=1
fi
}
need "EXPO_TOKEN" "${EXPO_TOKEN:-}"
need "APPLE_APP_STORE_CONNECT_API_KEY_ID" "${ASC_KEY_ID:-}"
need "APPLE_APP_STORE_CONNECT_ISSUER_ID" "${ASC_ISSUER_ID:-}"
need "APPLE_APP_STORE_CONNECT_API_KEY" "${ASC_KEY_B64:-}"
asc_app_id=$(node -p "require('./eas.json').submit.production.ios.ascAppId || ''")
team_id=$(node -p "require('./eas.json').submit.production.ios.appleTeamId || ''")
case "$asc_app_id" in
""|REPLACE_*) echo "::error::eas.json submit.production.ios.ascAppId is unset or still a placeholder"; fail=1 ;;
*[!0-9]*) echo "::error::eas.json submit.production.ios.ascAppId must contain only digits"; fail=1 ;;
esac
case "$team_id" in
""|REPLACE_*) echo "::error::eas.json submit.production.ios.appleTeamId is unset or still a placeholder"; fail=1 ;;
esac
if ! printf '%s' "$team_id" | grep -Eq '^[A-Z0-9]{10}$'; then
echo "::error::eas.json submit.production.ios.appleTeamId must be a 10-character Apple Team ID"
fail=1
fi
if [ -n "${ASC_KEY_ID:-}" ] && ! printf '%s' "$ASC_KEY_ID" | grep -Eq '^[A-Z0-9]{10}$'; then
echo "::error::APPLE_APP_STORE_CONNECT_API_KEY_ID must be a 10-character key ID"
fail=1
fi
if [ -n "${ASC_ISSUER_ID:-}" ] && ! printf '%s' "$ASC_ISSUER_ID" | grep -Eq '^[0-9a-fA-F-]{36}$'; then
echo "::error::APPLE_APP_STORE_CONNECT_ISSUER_ID must be a UUID"
fail=1
fi
if [ "$fail" -ne 0 ]; then
echo "::error::BLOCKED: complete the one-time human-gated setup in this workflow's header (GitHub secrets + eas.json identifiers) before releasing. No build was started."
exit 1
fi
echo "Preflight OK — all credentials and identifiers present."
- name: Install EAS CLI (exact pin)
run: npm install -g eas-cli@"$EAS_CLI_VERSION"
- name: Install dependencies (deterministic)
run: npm ci --legacy-peer-deps
- name: Configure App Store Connect API key
env:
ASC_KEY_ID: ${{ secrets.APPLE_APP_STORE_CONNECT_API_KEY_ID }}
ASC_ISSUER_ID: ${{ secrets.APPLE_APP_STORE_CONNECT_ISSUER_ID }}
ASC_KEY_B64: ${{ secrets.APPLE_APP_STORE_CONNECT_API_KEY }}
run: |
set -euo pipefail
key_path="$RUNNER_TEMP/asc_api_key.p8"
printf '%s' "$ASC_KEY_B64" | base64 -d > "$key_path"
if ! head -n1 "$key_path" | grep -q "BEGIN PRIVATE KEY"; then
echo "::error::APPLE_APP_STORE_CONNECT_API_KEY did not base64-decode to a valid .p8 private key."
exit 1
fi
chmod 600 "$key_path"
export ASC_KEY_PATH="$key_path"
# Expose ASC credentials to EAS Build for non-interactive signing management.
{
echo "EXPO_ASC_API_KEY_PATH=$key_path"
echo "EXPO_ASC_KEY_ID=$ASC_KEY_ID"
echo "EXPO_ASC_ISSUER_ID=$ASC_ISSUER_ID"
echo "EXPO_APPLE_TEAM_ID=$(node -p "require('./eas.json').submit.production.ios.appleTeamId")"
echo "EXPO_APPLE_TEAM_TYPE=COMPANY_OR_ORGANIZATION"
} >> "$GITHUB_ENV"
# EAS Submit reads the ASC key only from the eas.json submit profile (all three
# fields required). Inject them here so no real key IDs are committed to the repo.
node -e "
const fs = require('fs');
const j = require('./eas.json');
j.submit.production.ios.ascApiKeyPath = process.env.ASC_KEY_PATH;
j.submit.production.ios.ascApiKeyId = process.env.ASC_KEY_ID;
j.submit.production.ios.ascApiKeyIssuerId = process.env.ASC_ISSUER_ID;
fs.writeFileSync('eas.json', JSON.stringify(j, null, 2) + '\n');
"
echo "ASC API key configured for EAS Build and EAS Submit."
- name: EAS Build (iOS, wait for completion)
id: build
run: |
set -uo pipefail
set +e
eas build \
--platform ios \
--profile production \
--non-interactive \
--no-wait \
--json \
| tee eas-build-output.json
BUILD_ID=$(cat eas-build-output.json | node -e "const d=require('fs').readFileSync('/dev/stdin','utf8');console.log(JSON.parse(d).id)")
echo "EAS_BUILD_ID=$BUILD_ID" >> $GITHUB_ENV
echo "Build ID: $BUILD_ID"
--json > eas-build-output.json
rc=$?
set -e
if [ "$rc" -ne 0 ]; then
echo "::error::eas build failed (exit $rc). See the eas-ios-build-metadata artifact."
exit "$rc"
fi
# `eas build --json` prints a JSON ARRAY of completed builds. Select the exact
# iOS build id deterministically — never rely on an ambiguous "latest".
build_id=$(node -e "
const a = JSON.parse(require('fs').readFileSync('eas-build-output.json', 'utf8'));
if (!Array.isArray(a)) { console.error('Expected a JSON array from eas build --json'); process.exit(1); }
const ios = a.filter((b) => String(b.platform).toUpperCase() === 'IOS');
if (ios.length !== 1) { console.error('Expected exactly one iOS build, got ' + ios.length); process.exit(1); }
const b = ios[0];
if (b.status && String(b.status).toUpperCase() !== 'FINISHED') { console.error('iOS build did not finish: ' + b.status); process.exit(1); }
if (!b.id) { console.error('Build object has no id'); process.exit(1); }
process.stdout.write(b.id);
")
echo "build_id=$build_id" >> "$GITHUB_OUTPUT"
echo "Selected EAS iOS build id: $build_id"
# Wait for the EAS build to complete (iOS builds typically take 15–25 minutes).
- name: Wait for EAS build
if: steps.check-apple.outputs.proceed == 'true'
env:
EXPO_TOKEN: ${{ secrets.EAS_TOKEN }}
run: |
echo "Waiting for build $EAS_BUILD_ID to complete..."
eas build:view "$EAS_BUILD_ID" --json --wait
echo "Build complete."
- name: Upload EAS build metadata
if: always()
uses: actions/upload-artifact@v7
with:
name: eas-ios-build-metadata
path: eas-build-output.json
retention-days: 30
if-no-files-found: ignore
# Submit to TestFlight via EAS Submit. Uses the same App Store Connect API key.
# --latest: picks the most recent finished build for this app + platform.
- name: Submit to TestFlight via EAS Submit
if: steps.check-apple.outputs.proceed == 'true'
env:
EXPO_TOKEN: ${{ secrets.EAS_TOKEN }}
APPLE_APP_STORE_CONNECT_API_KEY_ID: ${{ secrets.APPLE_APP_STORE_CONNECT_API_KEY_ID }}
APPLE_APP_STORE_CONNECT_ISSUER_ID: ${{ secrets.APPLE_APP_STORE_CONNECT_ISSUER_ID }}
APPLE_APP_STORE_CONNECT_API_KEY: ${{ secrets.APPLE_APP_STORE_CONNECT_API_KEY }}
- name: Submit exact build to TestFlight
run: |
set -euo pipefail
eas submit \
--platform ios \
--id "$EAS_BUILD_ID" \
--profile production \
--id "${{ steps.build.outputs.build_id }}" \
--non-interactive
# Upload release notes to TestFlight (what's new text for testers).
# NOTE: EAS Submit does not yet support whatsNew natively; use fastlane pilot
# or App Store Connect API directly if per-build release notes are needed.
- name: Upload TestFlight release notes (informational)
if: steps.check-apple.outputs.proceed == 'true'
- name: TestFlight release notes (informational)
if: always()
run: |
echo "TestFlight release notes for this build:"
cat distribution/whatsnew-ios/release-notes-en-US.txt
# ---------------------------------------------------------------------------
# ALTERNATIVE: Self-hosted Mac runner (macbook13-pro at 100.68.120.26)
# ---------------------------------------------------------------------------
# To use the Mac mini instead of EAS Build:
# 1. SSH to macbook13-pro and set up GitHub self-hosted runner:
# https://docs.github.com/en/actions/hosting-your-own-runners/managing-self-hosted-runners/adding-self-hosted-runners
# 2. Change "runs-on: ubuntu-latest" above to "runs-on: self-hosted"
# and add label "macos" for clarity.
# 3. Replace the EAS Build + Submit steps with:
#
# - name: Install CocoaPods
# run: sudo gem install cocoapods
#
# - name: Expo prebuild (iOS)
# run: npx expo prebuild --platform ios --no-install
#
# - name: Install CocoaPods dependencies
# working-directory: ios
# run: pod install
#
# - name: Build IPA
# run: |
# xcodebuild -workspace ios/opencodemobile.xcworkspace \
# -scheme opencodemobile \
# -sdk iphoneos \
# -configuration Release \
# -archivePath $RUNNER_TEMP/opencodemobile.xcarchive \
# archive \
# CODE_SIGN_STYLE=Manual \
# DEVELOPMENT_TEAM=${{ secrets.APPLE_TEAM_ID }} \
# CODE_SIGN_IDENTITY="Apple Distribution" \
# PROVISIONING_PROFILE_SPECIFIER="${{ secrets.IOS_PROVISIONING_PROFILE_NAME }}"
#
# - name: Export IPA
# run: |
# xcodebuild -exportArchive \
# -archivePath $RUNNER_TEMP/opencodemobile.xcarchive \
# -exportOptionsPlist ios/ExportOptions.plist \
# -exportPath $RUNNER_TEMP/export
#
# - name: Upload to TestFlight (xcrun altool / notarytool)
# run: |
# xcrun altool --upload-app \
# -f "$RUNNER_TEMP/export/opencodemobile.ipa" \
# --type ios \
# --apiKey "${{ secrets.APPLE_APP_STORE_CONNECT_API_KEY_ID }}" \
# --apiIssuer "${{ secrets.APPLE_APP_STORE_CONNECT_ISSUER_ID }}"
#
# Self-hosted runner cost: $0 compute (your hardware), but requires maintaining
# a macOS machine with Xcode, certificates, and provisioning profiles.
# EAS Build is strongly recommended for the first release.
notes="distribution/whatsnew-ios/release-notes-en-US.txt"
if [ -f "$notes" ]; then
echo "TestFlight 'What to Test' notes for this release:"
cat "$notes"
else
echo "No release notes file found at $notes"
fi

View File

@@ -25,6 +25,7 @@
"NSMicrophoneUsageDescription": "OpenCode uses the microphone to capture your voice when using speech-to-text input.",
"NSPhotoLibraryUsageDescription": "OpenCode can attach images from your photo library to messages to your AI coding agent.",
"NSCameraUsageDescription": "OpenCode can capture images with your camera and attach them to messages to your AI coding agent.",
"NSLocalNetworkUsageDescription": "OpenCode uses your local network to connect to self-hosted OpenCode servers running on your LAN.",
"NSAppTransportSecurity": {
"NSAllowsArbitraryLoads": true,
"NSAllowsArbitraryLoadsInWebContent": false

Binary file not shown.

Before

Width:  |  Height:  |  Size: 57 KiB

After

Width:  |  Height:  |  Size: 28 KiB

View File

@@ -276,36 +276,28 @@ All icons and screenshots must be provided before submitting for review.
The 1024×1024 icon must NOT have rounded corners (Apple applies them). No transparency.
Current status: `assets/icon.json` is a placeholder — **real PNG required before submission**.
Current status: **Ready.** `assets/icon.png` is 1024×1024.
### iPhone Screenshots (REQUIRED)
Minimum 1 screenshot per device class. Recommended: 3–5 showing key flows.
| Device | Resolution | Size name in App Store Connect |
|---|---|---|
| iPhone 6.7" (iPhone 16 Pro Max / 15 Plus) | 1320×2868 or 1290×2796 | 6.7" Super Retina XDR Display |
| iPhone 6.5" (iPhone 14 Plus / 11 Pro Max) | 1242×2688 | 6.5" Super Retina XDR Display |
| iPhone 5.5" (iPhone 8 Plus) | 1242×2208 | 5.5" Retina HD Display |
| Device | Resolution | Size name in App Store Connect | Status |
|---|---|---|---|
| iPhone 6.7" (iPhone 16 Pro Max / 15 Plus) | 1320×2868 or 1290×2796 | 6.7" Super Retina XDR Display | Placeholder set exists; recapture from current iOS build |
| iPhone 6.5" (iPhone 14 Plus / 11 Pro Max) | 1242×2688 | 6.5" Super Retina XDR Display | Placeholder set exists; recapture from current iOS build |
| iPhone 5.5" (iPhone 8 Plus) | 1242×2208 | 5.5" Retina HD Display | Optional |
Note: As of 2024, Apple only requires 6.7" and 6.5" for new submissions. 5.5" is optional but recommended for coverage.
Suggested screenshot subjects:
1. Connection setup screen (add server URL)
2. Active chat session — streaming AI response
3. File diff view — seeing a code change
4. Tool approval dialog
5. Session list / multi-session view
6. Biometric unlock (if possible to screenshot without triggering auth)
### iPad Screenshots (REQUIRED for Universal apps)
Since `supportsTablet: true`, iPad screenshots are required.
| Device | Resolution | Size name in App Store Connect |
|---|---|---|
| iPad 12.9" (iPad Pro 6th gen) | 2048×2732 | 12.9" iPad Pro (6th gen) |
| iPad 11" (iPad Pro M4) | 1668×2388 | 11" iPad Pro (M4) |
| Device | Resolution | Size name in App Store Connect | Status |
|---|---|---|---|
| iPad 12.9" (iPad Pro 6th gen) | 2048×2732 | 12.9" iPad Pro (6th gen) | Placeholder set exists; recapture from current iOS build |
| iPad 11" (iPad Pro M4) | 1668×2388 | 11" iPad Pro (M4) | Optional |
Minimum 1 per device class required. iPad screenshots can be the same content as iPhone.
@@ -335,11 +327,11 @@ To use: you need opencode running somewhere accessible (local Wi-Fi, Tailscale,
## Pending Before First Submission
- [ ] Apple Developer Program enrollment approved (D-U-N-S 142059652, VIBE TECHNOLOGIES LLC)
- [ ] App Store Connect app record created (bundle ID: ai.opencode.mobile)
- [ ] App icon 1024×1024 PNG (no alpha, no rounded corners)
- [ ] iPhone screenshots (6.7" minimum; 6.5" strongly recommended)
- [ ] iPad screenshots (12.9" minimum)
- [ ] Privacy policy live at https://dzianisv.github.io/opencode-mobile/privacy/
- [ ] App Store Connect app record created (bundle ID: cc.agentlabs.opencode)
- [x] App icon 1024×1024 PNG (no alpha, no rounded corners)
- [ ] Capture current iPhone screenshots (6.7" minimum; 6.5" strongly recommended)
- [ ] Capture current iPad screenshots (12.9" minimum)
- [x] Privacy policy live at https://dzianisv.github.io/opencode-mobile/privacy/
- [ ] App Store Connect API key created (for CI — Key ID, Issuer ID, .p8 file)
- [ ] Apple Distribution certificate + provisioning profile (or use EAS managed signing)
- [ ] Export compliance answered (No to custom encryption)

View File

@@ -110,10 +110,10 @@ While waiting for Apple's verification call and approval:
- [x] Prepare App Store listing copy → `distribution/app-store-listing.md`
- [x] Write CI workflow (draft) → `.github/workflows/publish-app-store.yml`
- [ ] Create app icon 1024×1024 PNG
- [ ] Capture iPhone screenshots (use iOS Simulator in Xcode on any Mac)
- [ ] Capture iPad screenshots
- [ ] Write/publish privacy policy at https://dzianisv.github.io/opencode-mobile/privacy/
- [x] Create app icon 1024×1024 PNG
- [ ] Replace placeholder iPhone screenshots with captures from the current iOS Simulator build
- [ ] Replace placeholder iPad screenshots with captures from the current iOS Simulator build
- [x] Write/publish privacy policy at https://dzianisv.github.io/opencode-mobile/privacy/
- [ ] Set up EAS account at https://expo.dev/ (free tier, log in with Expo account)
- [ ] Add iOS config patches to `app.json` (done in this PR)
- [ ] Run `npx expo prebuild --platform ios` on a Mac to validate the Xcode project
@@ -127,8 +127,8 @@ While waiting for Apple's verification call and approval:
- Platform: iOS
- Name: `OpenCode`
- Primary Language: English (U.S.)
- Bundle ID: `ai.opencode.mobile` — register this explicit App ID first at https://developer.apple.com/account/resources/identifiers/
- SKU: `ai.opencode.mobile` (can match bundle ID)
- Bundle ID: `cc.agentlabs.opencode` — register this explicit App ID first at https://developer.apple.com/account/resources/identifiers/
- SKU: `cc.agentlabs.opencode` (can match bundle ID)
2. Configure App ID capabilities needed:
- Push Notifications (for `expo-notifications`)
@@ -141,7 +141,13 @@ While waiting for Apple's verification call and approval:
- Note: Key ID and Issuer ID
- Base64-encode the .p8 and store in GitHub secret `APPLE_APP_STORE_CONNECT_API_KEY`
4. Create an internal TestFlight group and add yourself as tester
4. Configure the EAS `production` environment for optional crash reporting:
- `EXPO_PUBLIC_SENTRY_DSN`
- `SENTRY_AUTH_TOKEN` (secret visibility)
- `SENTRY_ORG`
- `SENTRY_PROJECT`
5. Create an internal TestFlight group and add yourself as tester
---

View File

@@ -1,29 +1,33 @@
# Apple App Store — opencode-mobile
Operational doc for shipping `ai.opencode.mobile` to Apple App Store under VIBE TECHNOLOGIES, LLC.
Operational doc for shipping `cc.agentlabs.opencode` to Apple App Store under VIBE TECHNOLOGIES, LLC.
For full company facts (D-U-N-S, address, governor) see `~/.agents/skills/vibetechnologies-llc/SKILL.md`.
---
## Account state (as of 2026-05-24)
## Account state
The Apple account state below was last recorded on 2026-05-24. Re-verify it in the
Apple Developer portal before running the release workflow; this Linux runner has no
Apple or EAS credentials and cannot confirm enrollment status.
| Field | Value |
|---|---|
| Apple ID email | `support@agentlabs.cc` (per decision 2026-05-24) |
| Apple Developer Program | ❌ **not enrolled — user signing up now** |
| Apple Developer Program | ⚠️ Last recorded as not enrolled; verify current status |
| D-U-N-S (for org enrollment) | 142059652 |
| Enrollment fee | $99/year (not yet paid) |
| Identity verification call | ⏸ pending after enrollment submitted (Apple calls within 2-7 business days) |
| App Store Connect record | ⏸ created after enrollment |
| TestFlight | ⏸ available after enrollment |
| App Store production | ⏸ after TestFlight + Apple review |
| Enrollment fee | $99/year |
| Identity verification call | ⚠️ Verify current status |
| App Store Connect record | ⚠️ No app ID is configured in `eas.json` |
| TestFlight | ⏸ No verified build yet |
| App Store production | ⏸ After TestFlight + Apple review |
### Bundle identity
| Field | Value |
|---|---|
| Bundle identifier | `ai.opencode.mobile` (same as Android — same brand) |
| Bundle identifier | `cc.agentlabs.opencode` (same as Android) |
| Apple Team ID | ⏸ assigned at enrollment |
| App Store Connect App ID | ⏸ assigned on first app creation |
@@ -57,14 +61,14 @@ Because the answer is "No", no ERN (Encryption Registration Number) is required
## What's already done
1. ✅ iOS section of `app.json` patched:
- `ios.buildNumber`: "1" (CI auto-bumps)
- `ios.buildNumber`: "1" (initial value; EAS manages production build numbers remotely)
- `ios.entitlements.aps-environment`: "production" (push notifications)
- `ios.infoPlist.NSAppTransportSecurity.NSAllowsArbitraryLoads`: true (required — connects to user self-hosted opencode servers over HTTP on LAN)
- Usage strings: NSFaceIDUsageDescription, NSSpeechRecognitionUsageDescription, NSMicrophoneUsageDescription, NSPhotoLibraryUsageDescription, NSCameraUsageDescription
- Usage strings: NSFaceIDUsageDescription, NSSpeechRecognitionUsageDescription, NSMicrophoneUsageDescription, NSPhotoLibraryUsageDescription, NSCameraUsageDescription, NSLocalNetworkUsageDescription
- Plugin registrations completed for `expo-notifications`, `expo-image-picker`, `expo-speech-recognition` (were missing — would have caused native iOS setup to silently skip)
2. ✅ EAS Build config: `eas.json` with development/preview/production profiles (2 placeholders for App ID + Team ID)
3. ✅ Build strategy chosen: **EAS Build** (Expo cloud, free tier 30 builds/mo, managed certs, EAS Submit handles TestFlight upload)
4. ✅ CI workflow draft: `.github/workflows/publish-app-store.yml` (DRAFT — needs Apple secrets before enabling)
4. ✅ CI workflows: `.github/workflows/ios-ci.yml` validates unsigned Simulator builds; `.github/workflows/publish-app-store.yml` fails fast until Apple/EAS setup is complete
5. ✅ Listing copy drafted: `distribution/app-store-listing.md`
6. ✅ Enrollment runbook: `distribution/ios-enrollment-runbook.md` (pre-filled with all VIBE TECHNOLOGIES, LLC fields)
7. ✅ Release notes scaffold: `distribution/whatsnew-ios/release-notes-en-US.txt`
@@ -78,28 +82,28 @@ Because the answer is "No", no ERN (Encryption Registration Number) is required
| 1 | Sign in / create Apple ID for `support@agentlabs.cc` w/ 2FA | User | 🔴 user action required |
| 2 | Enroll in Apple Developer Program ($99) | User | 🔴 user action required |
| 3 | Pass Apple verification call | User | 🔴 user action required |
| 4 | App icon — 1024×1024 PNG, opaque (no alpha) | ✅ Done | `assets/icon-appstore.png` (flattened from Android-produced `assets/icon.png`) |
| 5 | iPhone screenshots 6.7" (1290×2796) + 6.5" (1242×2688) | ✅ Done | `distribution/app-store-graphics/iphone-67/{01,02,03}.png` + `iphone-65/` — 3 mockup screens: connection, chat, diff |
| 6 | iPad screenshots 12.9" (2048×2732) | ✅ Done | `distribution/app-store-graphics/ipad-129/{01,02}.png` — 2 mockup screens |
| 4 | App icon — 1024×1024 PNG, opaque (no alpha) | ✅ Done | `assets/icon.png` is RGB with no alpha channel |
| 5 | iPhone screenshots 6.7" (1290×2796) + 6.5" (1242×2688) | Mac | 🔴 Placeholder mockups exist; recapture the current app in Simulator |
| 6 | iPad screenshots 12.9" (2048×2732) | Mac | 🔴 Placeholder mockups exist; recapture the current app in Simulator |
| 7 | Privacy policy — live at https://dzianisv.github.io/opencode-mobile/privacy/ | ✅ done | Live & verified (HTTP 200) on gh-pages. Content handled by Android agent (`distribution/privacy-policy.{md,html}`). iOS-specific ATT / nutrition label addendum written in `distribution/app-store-listing.md`. |
| 8 | Privacy nutrition label (App Tracking + Data Collection) | ✅ Done | Updated in `distribution/app-store-listing.md` — ATT explicitly noted (not used), Sentry opt-in status documented |
| 9 | Export compliance | ✅ Done | `ITSAppUsesNonExemptEncryption: false` added to `app.json`. Answers + rationale in this doc (see Export Compliance section above) and `distribution/app-store-listing.md`. |
| 10 | ATS justification in App Review notes | ✅ Done | Full justification text in `distribution/app-store-listing.md` under "App Review Notes — ATS Justification" |
| 11 | Reviewer test instructions | ✅ Done | Updated with correct command (`opencode serve --hostname 0.0.0.0`) in `distribution/app-store-listing.md` |
| 12 | GitHub secrets: `EAS_TOKEN`, `APPLE_APP_STORE_CONNECT_API_KEY_ID`, `APPLE_APP_STORE_CONNECT_ISSUER_ID`, `APPLE_APP_STORE_CONNECT_API_KEY` (base64 .p8) | User | 🟡 post-enrollment — see `.github/workflows/publish-app-store.yml` header |
| 12 | GitHub secrets: `EXPO_TOKEN`, `APPLE_APP_STORE_CONNECT_API_KEY_ID`, `APPLE_APP_STORE_CONNECT_ISSUER_ID`, `APPLE_APP_STORE_CONNECT_API_KEY` (base64 .p8) | User | 🟡 post-enrollment — see `.github/workflows/publish-app-store.yml` header |
| 13 | Update `eas.json` placeholders: `ascAppId` + `appleTeamId` | User | 🟡 post-enrollment — see `eas.json.README.md` for click paths |
| 14 | CI workflow validated | ✅ Done | `.github/workflows/publish-app-store.yml` structure verified; comment header updated with remaining gaps |
| 14 | CI workflow validated | CI | 🟡 Linux checks pass; PR must prove the macOS Simulator build |
| 15 | TestFlight release notes | ✅ Done | `distribution/whatsnew-ios/release-notes-en-US.txt` — polished, 1658 chars (limit 4000) |
---
## Publishing process (after enrollment + assets ready)
1. (manual) Sign in to App Store Connect, create app with bundle id `ai.opencode.mobile`.
1. (manual) Sign in to App Store Connect, create app with bundle id `cc.agentlabs.opencode`.
2. (manual) Generate App Store Connect API key (App Manager role) → download `.p8` → base64 encode → add as GitHub secret.
3. (manual) Update `eas.json` placeholders (Team ID, ASC App ID).
4. (manual) `eas login` + `eas build:configure` for first-time setup (managed signing).
5. (automated) `git tag v0.2.x && git push --tags` → CI calls EAS Build → EAS Submit → IPA lands in TestFlight.
5. (automated) Publish a GitHub Release for the version tag → CI calls EAS Build → EAS Submit → IPA lands in TestFlight.
6. (manual, first time) Add internal testers in App Store Connect → distribute via TestFlight.
7. (manual) After internal testing OK → submit for App Store review (production).
8. Apple review typically 24-48h. 90% of submissions reviewed within 24h.
@@ -138,7 +142,8 @@ Upgrade to EAS $19/mo only if free-tier queue (10-30 min wait) becomes a problem
- `app.json` — iOS config (patched 2026-05-24)
- `eas.json` — EAS build profiles (2 placeholders)
- `.github/workflows/publish-app-store.yml` — DRAFT CI
- `.github/workflows/ios-ci.yml` — PR/main unsigned iOS Simulator build gate
- `.github/workflows/publish-app-store.yml` — fail-fast TestFlight release CI
- `distribution/app-store-listing.md` — listing copy + answers
- `distribution/ios-enrollment-runbook.md` — enrollment runbook
- `distribution/whatsnew-ios/release-notes-en-US.txt` — release notes

View File

@@ -1,6 +1,7 @@
{
"cli": {
"version": ">= 13.0.0"
"version": ">= 21.0.0",
"appVersionSource": "remote"
},
"build": {
"development": {
@@ -17,9 +18,10 @@
}
},
"production": {
"autoIncrement": false,
"environment": "production",
"ios": {
"distribution": "store"
"distribution": "store",
"autoIncrement": "buildNumber"
},
"android": {
"buildType": "app-bundle"

View File

@@ -62,20 +62,21 @@ Alternatively, in App Store Connect:
| Field | Value | Notes |
|---|---|---|
| `appleId` | `appstore@agentlabs.cc` | The Apple ID used for App Store Connect login — update if different |
| `appleId` | `support@agentlabs.cc` | The Apple ID used for App Store Connect login — update if different |
| `distribution` (production ios) | `store` | Correct for App Store / TestFlight submissions |
| `buildType` (production android) | `app-bundle` | Correct for Play Store AAB submissions |
| `autoIncrement` | `false` | Build number is bumped by the CI workflow (github.run_number), not EAS |
| `cli.version` | `>= 13.0.0` | Requires EAS CLI 13 or later; CI installs `eas-cli@13` |
| `autoIncrement` | `buildNumber` | EAS increments the iOS build number remotely for every production build |
| `appVersionSource` | `remote` | EAS is the source of truth for store build numbers |
| `cli.version` | `>= 21.0.0` | CI installs the exact supported release, `eas-cli@21.0.0` |
---
## After filling in the placeholders
1. Commit the updated `eas.json` to the repo.
2. Add the GitHub Actions secrets (see `.github/workflows/publish-app-store.yml` header for the exact list).
3. Tag a release: `git tag v0.2.3 && git push --tags`
4. The CI workflow will build the IPA via EAS and submit it to TestFlight automatically.
2. Add the `EXPO_TOKEN` and App Store Connect API GitHub Actions secrets (see `.github/workflows/publish-app-store.yml` for the exact list).
3. Publish a GitHub Release for the version tag (or manually dispatch the App Store workflow).
4. The release event triggers CI to build the IPA via EAS and submit that exact build to TestFlight.
---