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. # Publish OpenCode for iOS to TestFlight with EAS Build + EAS Submit.
# Once enrollment is approved, complete these steps and this workflow is production-ready:
# #
# REMAINING GAPS (must complete before first run): # This workflow FAILS FAST (non-zero exit) instead of "succeeding by skipping":
# 1. Update eas.json: replace REPLACE_WITH_APP_STORE_CONNECT_APP_ID and REPLACE_WITH_APPLE_TEAM_ID # a release with missing credentials or unfilled identifiers is a hard error, so a
# (see eas.json.README.md for exact click paths in App Store Connect) # green run always means a real build was produced and submitted.
# 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)
# #
# OPTIONAL secrets (crash reporting): # ── HUMAN GATE (one-time, after Apple Developer Program enrollment) ──────────────
# EXPO_PUBLIC_SENTRY_DSN SENTRY_AUTH_TOKEN SENTRY_ORG SENTRY_PROJECT # Complete ALL of the following before releasing. Do NOT invent any of these IDs.
# #
# Build strategy: EAS Build (Expo Application Services) # 1. Fill and commit the eas.json placeholders (see eas.json.README.md for click paths):
# - No Mac runner needed; Expo hosts macOS workers with managed certificates. # submit.production.ios.ascAppId REPLACE_WITH_APP_STORE_CONNECT_APP_ID → numeric App Store Connect App ID
# - Cost: free tier (30 builds/month); upgrade to $19/month for unlimited/priority queue. # submit.production.ios.appleTeamId REPLACE_WITH_APPLE_TEAM_ID → 10-char Apple Team ID
# - See distribution/ios-enrollment-runbook.md for full enrollment steps. #
# - See eas.json.README.md for placeholder fill-in instructions. # 2. Add GitHub Actions secrets (Settings → Secrets and variables → Actions):
# - Alternative (self-hosted Mac runner): see commented section at bottom of this file. # 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) name: Publish to App Store (TestFlight)
on: on:
# One release ⇒ one build. Triggering only on `release: published` avoids the
# duplicate build that a combined release+tag trigger would create.
release: release:
types: [published] types: [published]
push:
tags: ["v*"]
workflow_dispatch: workflow_dispatch:
jobs: # Serialize runs per release so a re-trigger cannot start a duplicate concurrent
publish-ios: # build/submit. cancel-in-progress:false never kills an in-flight submission.
runs-on: ubuntu-latest concurrency:
env: group: publish-app-store-${{ github.event.release.tag_name || github.ref_name }}
EXPO_PUBLIC_SENTRY_DSN: ${{ secrets.EXPO_PUBLIC_SENTRY_DSN }} cancel-in-progress: false
SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
SENTRY_ORG: ${{ secrets.SENTRY_ORG }}
SENTRY_PROJECT: ${{ secrets.SENTRY_PROJECT }}
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: steps:
- uses: actions/checkout@v6 - uses: actions/checkout@v6
@@ -48,144 +62,148 @@ jobs:
node-version: 20 node-version: 20
cache: npm cache: npm
- name: Check Apple prerequisites - name: Preflight — verify credentials and identifiers (fail fast)
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'
env: env:
EXPO_TOKEN: ${{ secrets.EAS_TOKEN }} ASC_KEY_ID: ${{ secrets.APPLE_APP_STORE_CONNECT_API_KEY_ID }}
APPLE_APP_STORE_CONNECT_API_KEY_ID: ${{ secrets.APPLE_APP_STORE_CONNECT_API_KEY_ID }} ASC_ISSUER_ID: ${{ secrets.APPLE_APP_STORE_CONNECT_ISSUER_ID }}
APPLE_APP_STORE_CONNECT_ISSUER_ID: ${{ secrets.APPLE_APP_STORE_CONNECT_ISSUER_ID }} ASC_KEY_B64: ${{ secrets.APPLE_APP_STORE_CONNECT_API_KEY }}
APPLE_APP_STORE_CONNECT_API_KEY: ${{ secrets.APPLE_APP_STORE_CONNECT_API_KEY }}
run: | 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 \ eas build \
--platform ios \ --platform ios \
--profile production \ --profile production \
--non-interactive \ --non-interactive \
--no-wait \ --json > eas-build-output.json
--json \ rc=$?
| tee eas-build-output.json set -e
BUILD_ID=$(cat eas-build-output.json | node -e "const d=require('fs').readFileSync('/dev/stdin','utf8');console.log(JSON.parse(d).id)") if [ "$rc" -ne 0 ]; then
echo "EAS_BUILD_ID=$BUILD_ID" >> $GITHUB_ENV echo "::error::eas build failed (exit $rc). See the eas-ios-build-metadata artifact."
echo "Build ID: $BUILD_ID" 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: Upload EAS build metadata
- name: Wait for EAS build if: always()
if: steps.check-apple.outputs.proceed == 'true' uses: actions/upload-artifact@v7
env: with:
EXPO_TOKEN: ${{ secrets.EAS_TOKEN }} name: eas-ios-build-metadata
run: | path: eas-build-output.json
echo "Waiting for build $EAS_BUILD_ID to complete..." retention-days: 30
eas build:view "$EAS_BUILD_ID" --json --wait if-no-files-found: ignore
echo "Build complete."
# Submit to TestFlight via EAS Submit. Uses the same App Store Connect API key. - name: Submit exact build to TestFlight
# --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 }}
run: | run: |
set -euo pipefail
eas submit \ eas submit \
--platform ios \ --platform ios \
--id "$EAS_BUILD_ID" \ --profile production \
--id "${{ steps.build.outputs.build_id }}" \
--non-interactive --non-interactive
# Upload release notes to TestFlight (what's new text for testers). - name: TestFlight release notes (informational)
# NOTE: EAS Submit does not yet support whatsNew natively; use fastlane pilot if: always()
# 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'
run: | run: |
echo "TestFlight release notes for this build:" notes="distribution/whatsnew-ios/release-notes-en-US.txt"
cat distribution/whatsnew-ios/release-notes-en-US.txt if [ -f "$notes" ]; then
echo "TestFlight 'What to Test' notes for this release:"
# --------------------------------------------------------------------------- cat "$notes"
# ALTERNATIVE: Self-hosted Mac runner (macbook13-pro at 100.68.120.26) else
# --------------------------------------------------------------------------- echo "No release notes file found at $notes"
# To use the Mac mini instead of EAS Build: fi
# 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.

View File

@@ -25,6 +25,7 @@
"NSMicrophoneUsageDescription": "OpenCode uses the microphone to capture your voice when using speech-to-text input.", "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.", "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.", "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": { "NSAppTransportSecurity": {
"NSAllowsArbitraryLoads": true, "NSAllowsArbitraryLoads": true,
"NSAllowsArbitraryLoadsInWebContent": false "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. 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) ### iPhone Screenshots (REQUIRED)
Minimum 1 screenshot per device class. Recommended: 3–5 showing key flows. Minimum 1 screenshot per device class. Recommended: 3–5 showing key flows.
| Device | Resolution | Size name in App Store Connect | | 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 | | 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 | | 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 | | 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. 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) ### iPad Screenshots (REQUIRED for Universal apps)
Since `supportsTablet: true`, iPad screenshots are required. Since `supportsTablet: true`, iPad screenshots are required.
| Device | Resolution | Size name in App Store Connect | | Device | Resolution | Size name in App Store Connect | Status |
|---|---|---| |---|---|---|---|
| iPad 12.9" (iPad Pro 6th gen) | 2048×2732 | 12.9" iPad Pro (6th gen) | | 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) | | 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. 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 ## Pending Before First Submission
- [ ] Apple Developer Program enrollment approved (D-U-N-S 142059652, VIBE TECHNOLOGIES LLC) - [ ] 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 Store Connect app record created (bundle ID: cc.agentlabs.opencode)
- [ ] App icon 1024×1024 PNG (no alpha, no rounded corners) - [x] App icon 1024×1024 PNG (no alpha, no rounded corners)
- [ ] iPhone screenshots (6.7" minimum; 6.5" strongly recommended) - [ ] Capture current iPhone screenshots (6.7" minimum; 6.5" strongly recommended)
- [ ] iPad screenshots (12.9" minimum) - [ ] Capture current iPad screenshots (12.9" minimum)
- [ ] Privacy policy live at https://dzianisv.github.io/opencode-mobile/privacy/ - [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) - [ ] App Store Connect API key created (for CI — Key ID, Issuer ID, .p8 file)
- [ ] Apple Distribution certificate + provisioning profile (or use EAS managed signing) - [ ] Apple Distribution certificate + provisioning profile (or use EAS managed signing)
- [ ] Export compliance answered (No to custom encryption) - [ ] 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] Prepare App Store listing copy → `distribution/app-store-listing.md`
- [x] Write CI workflow (draft) → `.github/workflows/publish-app-store.yml` - [x] Write CI workflow (draft) → `.github/workflows/publish-app-store.yml`
- [ ] Create app icon 1024×1024 PNG - [x] Create app icon 1024×1024 PNG
- [ ] Capture iPhone screenshots (use iOS Simulator in Xcode on any Mac) - [ ] Replace placeholder iPhone screenshots with captures from the current iOS Simulator build
- [ ] Capture iPad screenshots - [ ] Replace placeholder iPad screenshots with captures from the current iOS Simulator build
- [ ] Write/publish privacy policy at https://dzianisv.github.io/opencode-mobile/privacy/ - [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) - [ ] 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) - [ ] 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 - [ ] 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 - Platform: iOS
- Name: `OpenCode` - Name: `OpenCode`
- Primary Language: English (U.S.) - Primary Language: English (U.S.)
- Bundle ID: `ai.opencode.mobile` — register this explicit App ID first at https://developer.apple.com/account/resources/identifiers/ - Bundle ID: `cc.agentlabs.opencode` — register this explicit App ID first at https://developer.apple.com/account/resources/identifiers/
- SKU: `ai.opencode.mobile` (can match bundle ID) - SKU: `cc.agentlabs.opencode` (can match bundle ID)
2. Configure App ID capabilities needed: 2. Configure App ID capabilities needed:
- Push Notifications (for `expo-notifications`) - Push Notifications (for `expo-notifications`)
@@ -141,7 +141,13 @@ While waiting for Apple's verification call and approval:
- Note: Key ID and Issuer ID - Note: Key ID and Issuer ID
- Base64-encode the .p8 and store in GitHub secret `APPLE_APP_STORE_CONNECT_API_KEY` - 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 # 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`. 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 | | Field | Value |
|---|---| |---|---|
| Apple ID email | `support@agentlabs.cc` (per decision 2026-05-24) | | 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 | | D-U-N-S (for org enrollment) | 142059652 |
| Enrollment fee | $99/year (not yet paid) | | Enrollment fee | $99/year |
| Identity verification call | ⏸ pending after enrollment submitted (Apple calls within 2-7 business days) | | Identity verification call | ⚠️ Verify current status |
| App Store Connect record | ⏸ created after enrollment | | App Store Connect record | ⚠️ No app ID is configured in `eas.json` |
| TestFlight | ⏸ available after enrollment | | TestFlight | ⏸ No verified build yet |
| App Store production | ⏸ after TestFlight + Apple review | | App Store production | ⏸ After TestFlight + Apple review |
### Bundle identity ### Bundle identity
| Field | Value | | 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 | | Apple Team ID | ⏸ assigned at enrollment |
| App Store Connect App ID | ⏸ assigned on first app creation | | 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 ## What's already done
1. ✅ iOS section of `app.json` patched: 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.entitlements.aps-environment`: "production" (push notifications)
- `ios.infoPlist.NSAppTransportSecurity.NSAllowsArbitraryLoads`: true (required — connects to user self-hosted opencode servers over HTTP on LAN) - `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) - 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) 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) 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` 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) 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` 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 | | 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 | | 2 | Enroll in Apple Developer Program ($99) | User | 🔴 user action required |
| 3 | Pass Apple verification call | 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`) | | 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) | ✅ Done | `distribution/app-store-graphics/iphone-67/{01,02,03}.png` + `iphone-65/` — 3 mockup screens: connection, chat, diff | | 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) | ✅ Done | `distribution/app-store-graphics/ipad-129/{01,02}.png` — 2 mockup screens | | 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`. | | 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 | | 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`. | | 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" | | 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` | | 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 | | 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) | | 15 | TestFlight release notes | ✅ Done | `distribution/whatsnew-ios/release-notes-en-US.txt` — polished, 1658 chars (limit 4000) |
--- ---
## Publishing process (after enrollment + assets ready) ## 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. 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). 3. (manual) Update `eas.json` placeholders (Team ID, ASC App ID).
4. (manual) `eas login` + `eas build:configure` for first-time setup (managed signing). 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. 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). 7. (manual) After internal testing OK → submit for App Store review (production).
8. Apple review typically 24-48h. 90% of submissions reviewed within 24h. 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) - `app.json` — iOS config (patched 2026-05-24)
- `eas.json` — EAS build profiles (2 placeholders) - `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/app-store-listing.md` — listing copy + answers
- `distribution/ios-enrollment-runbook.md` — enrollment runbook - `distribution/ios-enrollment-runbook.md` — enrollment runbook
- `distribution/whatsnew-ios/release-notes-en-US.txt` — release notes - `distribution/whatsnew-ios/release-notes-en-US.txt` — release notes

View File

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

View File

@@ -62,20 +62,21 @@ Alternatively, in App Store Connect:
| Field | Value | Notes | | 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 | | `distribution` (production ios) | `store` | Correct for App Store / TestFlight submissions |
| `buildType` (production android) | `app-bundle` | Correct for Play Store AAB 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 | | `autoIncrement` | `buildNumber` | EAS increments the iOS build number remotely for every production build |
| `cli.version` | `>= 13.0.0` | Requires EAS CLI 13 or later; CI installs `eas-cli@13` | | `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 ## After filling in the placeholders
1. Commit the updated `eas.json` to the repo. 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). 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. Tag a release: `git tag v0.2.3 && git push --tags` 3. Publish a GitHub Release for the version tag (or manually dispatch the App Store workflow).
4. The CI workflow will build the IPA via EAS and submit it to TestFlight automatically. 4. The release event triggers CI to build the IPA via EAS and submit that exact build to TestFlight.
--- ---