diff --git a/.github/workflows/ios-ci.yml b/.github/workflows/ios-ci.yml new file mode 100644 index 0000000..152fff4 --- /dev/null +++ b/.github/workflows/ios-ci.yml @@ -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 diff --git a/.github/workflows/publish-app-store.yml b/.github/workflows/publish-app-store.yml index 2426770..97e7bdf 100644 --- a/.github/workflows/publish-app-store.yml +++ b/.github/workflows/publish-app-store.yml @@ -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 diff --git a/app.json b/app.json index 5bfcaf1..f66fb07 100644 --- a/app.json +++ b/app.json @@ -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 diff --git a/assets/icon.png b/assets/icon.png index 9e16101..e13b95d 100644 Binary files a/assets/icon.png and b/assets/icon.png differ diff --git a/distribution/app-store-listing.md b/distribution/app-store-listing.md index ee59ecb..6566c31 100644 --- a/distribution/app-store-listing.md +++ b/distribution/app-store-listing.md @@ -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) diff --git a/distribution/ios-enrollment-runbook.md b/distribution/ios-enrollment-runbook.md index c57a2ee..08e3bb0 100644 --- a/distribution/ios-enrollment-runbook.md +++ b/distribution/ios-enrollment-runbook.md @@ -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 --- diff --git a/docs/applestore.md b/docs/applestore.md index 8f56f95..fd6a333 100644 --- a/docs/applestore.md +++ b/docs/applestore.md @@ -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 diff --git a/eas.json b/eas.json index 4d6c8e7..b8bbb49 100644 --- a/eas.json +++ b/eas.json @@ -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" diff --git a/eas.json.README.md b/eas.json.README.md index 76f5353..5dde065 100644 --- a/eas.json.README.md +++ b/eas.json.README.md @@ -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. ---