Repository navigation
iOS: public App Store lane (com.cmux.app), privacy manifest, fastlane screenshots #6697
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 6 commits
651c241
e240275
7134e05
94a3f29
2fce840
914de8b
0f928d5
98d58b5
a5342f9
94898be
b3be04d
d3a23e2
0b9930e
2d70f35
42bb732
82c69c3
8141521
ae40c0e
4ace31a
4a3139a
654d95f
dc35f40
91c788b
e208826
633bd59
e6c5cd4
2ff4250
0775c11
9eb533d
0373057
232bc55
ab305c9
0b54405
c85b09d
81617d6
dc1456f
8821c1c
68971db
c49d700
81fc984
0e9e5d7
6850b67
6c09a33
f247cbd
e874a7f
192b37f
23a5a76
3984f35
f89dc2f
c656c82
f8bf46d
8d2b892
4bb8f38
a2d3640
d42e902
cef5511
335a287
d770c40
969cca8
51d3c87
9127e2f
806ee4f
f60d73d
9854b9d
019458f
fe3b0e6
d1d4cb7
2a36c2b
216598a
3988950
be5be9f
694d57d
9dbb52a
8f61bfb
11a1323
0ead529
94014d9
16c485c
aa28a59
3ed49c8
35bb4ac
ae22180
4692116
090ff08
33410ea
d0f3a4f
32ff0e4
ea1ef25
902a1da
8e690b3
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,196 @@ | ||
| name: iOS App Store screenshots | ||
|
|
||
| # Captures fastlane screenshots for the public App Store listing (com.cmux.app) | ||
| # on the required device classes — 6.9" iPhone + 13" iPad — in every locale the | ||
| # app is localized in (en-US + ja). The capture runs against a DEBUG build with | ||
| # the CMUX_UITEST_MOCK_DATA hook, so no Apple signing is needed to shoot; only | ||
| # the optional upload-to-ASC step needs the App Store Connect API key. | ||
| # | ||
| # On PRs touching the screenshot tooling it runs capture-only (validates the | ||
| # snapshot test + Snapfile). Upload to App Store Connect is workflow_dispatch + | ||
| # upload=true only. | ||
|
|
||
| on: | ||
| workflow_dispatch: | ||
| inputs: | ||
| ref: | ||
| description: Branch or SHA to capture from | ||
| required: false | ||
| default: "" | ||
| languages: | ||
| description: "Comma-separated App Store locales (app is localized in en-US, ja)" | ||
| required: false | ||
| default: "en-US,ja" | ||
| upload: | ||
| description: "Upload captured screenshots to the com.cmux.app App Store listing" | ||
| required: false | ||
| default: false | ||
| type: boolean | ||
| pull_request: | ||
| paths: | ||
| - "ios/fastlane/**" | ||
| - "ios/cmuxUITests/SnapshotUITests.swift" | ||
| - "ios/cmuxUITests/SnapshotHelper.swift" | ||
| - "ios/Gemfile" | ||
| - ".github/workflows/ios-screenshots.yml" | ||
|
|
||
| permissions: | ||
| contents: read | ||
|
|
||
| jobs: | ||
|
coderabbitai[bot] marked this conversation as resolved.
Resolved
|
||
| screenshots: | ||
| # Same lane as the iOS simulator tests: screenshots run inside the Simulator, | ||
| # not as a foregrounded Mac app, so the Blacksmith foreground limitation does | ||
| # not apply. | ||
| runs-on: ${{ vars.MACOS_RUNNER_IOS || 'blacksmith-6vcpu-macos-26' }} | ||
|
cursor[bot] marked this conversation as resolved.
Resolved
|
||
| timeout-minutes: 60 | ||
| steps: | ||
| - name: Checkout | ||
| uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 | ||
| with: | ||
| ref: ${{ inputs.ref || github.ref }} | ||
|
cubic-dev-ai[bot] marked this conversation as resolved.
Resolved
|
||
| persist-credentials: false | ||
| submodules: recursive | ||
|
|
||
| - name: Select Xcode | ||
| run: | | ||
| set -euo pipefail | ||
| if [ -d "/Applications/Xcode.app/Contents/Developer" ]; then | ||
| XCODE_DIR="/Applications/Xcode.app/Contents/Developer" | ||
| else | ||
| XCODE_APP="$(find /Applications -maxdepth 1 -type d -name 'Xcode*.app' -print 2>/dev/null | sort | tail -n 1 || true)" | ||
| [ -n "$XCODE_APP" ] || { echo "No Xcode.app found" >&2; exit 1; } | ||
| XCODE_DIR="$XCODE_APP/Contents/Developer" | ||
| fi | ||
| echo "DEVELOPER_DIR=$XCODE_DIR" >> "$GITHUB_ENV" | ||
| export DEVELOPER_DIR="$XCODE_DIR" | ||
| xcodebuild -version | ||
|
cursor[bot] marked this conversation as resolved.
Resolved
|
||
|
|
||
| - name: Install zig | ||
| run: ./scripts/install-zig-ci.sh | ||
|
|
||
| - name: Ensure iOS simulator runtime | ||
| run: | | ||
| set -euo pipefail | ||
| if xcrun simctl list runtimes available | grep -Eq '\biOS\b'; then exit 0; fi | ||
| echo "No available iOS simulator runtime; downloading the iOS platform." | ||
| xcodebuild -downloadPlatform iOS | ||
| xcrun simctl list runtimes available | grep -Eq '\biOS\b' | ||
|
|
||
| - name: Provision GhosttyKit | ||
| run: ./scripts/ensure-ghosttykit.sh | ||
|
|
||
| - name: Resolve screenshot devices (6.9" iPhone + 13" iPad) | ||
| run: | | ||
| set -euo pipefail | ||
| # Device model names drift between Xcode versions, so resolve the | ||
| # required App Store classes at runtime instead of pinning. Pick from | ||
| # the AVAILABLE SIMULATORS (not device types) so fastlane finds an | ||
| # existing simulator, and skip RAM-variant duplicates like | ||
| # "iPad Pro 13-inch (M5) (16GB)" which have no pre-created simulator. | ||
| python3 - <<'PY' >> "$GITHUB_ENV" | ||
| import json, re, subprocess | ||
|
|
||
| def simctl(*args): | ||
| return json.loads(subprocess.check_output(["xcrun", "simctl", "list", *args, "-j"])) | ||
|
|
||
| def vkey(s): | ||
| return [int(x) for x in re.findall(r"\d+", s or "")] or [0] | ||
|
|
||
| data = simctl("devices", "available") | ||
| names = sorted({ | ||
| d["name"] | ||
| for runtime in data.get("devices", {}).values() | ||
| for d in runtime | ||
| if d.get("isAvailable", True) | ||
| }) | ||
|
|
||
| def pick(patterns, fallbacks): | ||
| for pat in patterns: | ||
| hits = sorted([n for n in names if re.fullmatch(pat, n)], key=vkey) | ||
| if hits: | ||
| return hits[-1] | ||
| for f in fallbacks: | ||
| hits = sorted([n for n in names if f(n)], key=vkey) | ||
| if hits: | ||
| return hits[-1] | ||
| return None | ||
|
|
||
| # 6.9" iPhone (Pro Max) and 13" iPad (Pro, else Air). "GB" filters out | ||
| # RAM-variant names that are not instantiated as simulators. | ||
| iphone = pick([r"iPhone \d+ Pro Max"], | ||
| [lambda n: n.startswith("iPhone") and "Pro Max" in n and "GB" not in n, | ||
| lambda n: n.startswith("iPhone") and "GB" not in n]) | ||
| ipad = pick([r"iPad Pro 13-inch \(M\d+\)", r"iPad Air 13-inch \(M\d+\)"], | ||
| [lambda n: n.startswith("iPad") and "13-inch" in n and "GB" not in n, | ||
| lambda n: n.startswith("iPad") and "GB" not in n]) | ||
|
cursor[bot] marked this conversation as resolved.
Resolved
|
||
| if not iphone or not ipad: | ||
| raise SystemExit("Could not resolve iPhone/iPad from available simulators: " + ", ".join(names)) | ||
| print(f"SNAPSHOT_DEVICES={iphone},{ipad}") | ||
| PY | ||
| # $SNAPSHOT_DEVICES is only in $GITHUB_ENV for later steps, not this | ||
| # shell, so read it back from the file rather than referencing the var | ||
| # (which would trip `set -u`). | ||
| grep '^SNAPSHOT_DEVICES=' "$GITHUB_ENV" || { echo "device resolution failed" >&2; exit 1; } | ||
|
|
||
|
coderabbitai[bot] marked this conversation as resolved.
Resolved
|
||
| - name: Ensure fastlane | ||
| run: | | ||
| set -euo pipefail | ||
| # Use the Homebrew fastlane (ships its own Ruby) instead of bundler: | ||
| # macOS system Ruby is 2.6 but fastlane needs >= 3.0, so `bundle install` | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
The Python heredoc writes |
||
| # against system Ruby can't resolve. This matches the local dev setup. | ||
| if ! command -v fastlane >/dev/null 2>&1; then | ||
| brew install fastlane | ||
| fi | ||
| fastlane --version | ||
|
|
||
| - name: Capture screenshots | ||
| working-directory: ios | ||
| env: | ||
| SNAPSHOT_LANGUAGES: ${{ inputs.languages || 'en-US,ja' }} | ||
| FASTLANE_SKIP_UPDATE_CHECK: "1" | ||
| FASTLANE_HIDE_CHANGELOG: "1" | ||
| run: | | ||
| set -euo pipefail | ||
| echo "Devices: $SNAPSHOT_DEVICES" | ||
| echo "Languages: $SNAPSHOT_LANGUAGES" | ||
| fastlane screenshots | ||
|
cursor[bot] marked this conversation as resolved.
Resolved
|
||
|
|
||
| - name: Upload screenshots artifact | ||
| if: ${{ always() }} | ||
| uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 | ||
| with: | ||
| name: ios-appstore-screenshots | ||
| path: ios/fastlane/screenshots | ||
| if-no-files-found: warn | ||
| retention-days: 14 | ||
|
|
||
| - name: Materialize App Store Connect API key | ||
| if: ${{ github.event_name == 'workflow_dispatch' && inputs.upload }} | ||
| env: | ||
| ASC_API_KEY_ID: ${{ secrets.ASC_API_KEY_ID }} | ||
| ASC_API_ISSUER_ID: ${{ secrets.ASC_API_ISSUER_ID }} | ||
| ASC_API_KEY_P8_BASE64: ${{ secrets.ASC_API_KEY_P8_BASE64 }} | ||
| run: | | ||
| set -euo pipefail | ||
| if [ -z "${ASC_API_KEY_ID:-}" ] || [ -z "${ASC_API_ISSUER_ID:-}" ] || [ -z "${ASC_API_KEY_P8_BASE64:-}" ]; then | ||
| echo "Missing ASC_API_KEY_ID / ASC_API_ISSUER_ID / ASC_API_KEY_P8_BASE64 secrets" >&2 | ||
| exit 1 | ||
| fi | ||
| KEY_DIR="$HOME/.appstoreconnect/private_keys" | ||
| mkdir -p "$KEY_DIR" | ||
| printf '%s' "$ASC_API_KEY_P8_BASE64" | base64 --decode > "$KEY_DIR/AuthKey_${ASC_API_KEY_ID}.p8" | ||
| chmod 600 "$KEY_DIR/AuthKey_${ASC_API_KEY_ID}.p8" | ||
| { | ||
| echo "ASC_API_KEY_ID=$ASC_API_KEY_ID" | ||
| echo "ASC_API_ISSUER_ID=$ASC_API_ISSUER_ID" | ||
| echo "ASC_API_KEY_PATH=$KEY_DIR/AuthKey_${ASC_API_KEY_ID}.p8" | ||
| } >> "$GITHUB_ENV" | ||
|
|
||
| - name: Upload screenshots to App Store Connect | ||
| if: ${{ github.event_name == 'workflow_dispatch' && inputs.upload }} | ||
| working-directory: ios | ||
| env: | ||
| FASTLANE_SKIP_UPDATE_CHECK: "1" | ||
| FASTLANE_HIDE_CHANGELOG: "1" | ||
| run: fastlane upload_screenshots | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,56 @@ | ||
| <?xml version="1.0" encoding="UTF-8"?> | ||
| <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> | ||
| <plist version="1.0"> | ||
| <dict> | ||
| <!-- cmux does not track users across apps/websites and embeds no ad/tracking SDKs. --> | ||
| <key>NSPrivacyTracking</key> | ||
| <false/> | ||
| <key>NSPrivacyTrackingDomains</key> | ||
| <array/> | ||
|
|
||
| <!-- First-party data collection. Crash/diagnostics SDKs (e.g. Sentry) are not | ||
| linked into the iOS app; analytics are first-party product-interaction | ||
| events sent to the cmux web API (server-side PostHog proxy), gated by an | ||
| in-app opt-out. Keep in lockstep with the App Store Connect privacy label. --> | ||
| <key>NSPrivacyCollectedDataTypes</key> | ||
| <array> | ||
| <dict> | ||
| <key>NSPrivacyCollectedDataType</key> | ||
| <string>NSPrivacyCollectedDataTypeProductInteraction</string> | ||
| <key>NSPrivacyCollectedDataTypeLinked</key> | ||
| <true/> | ||
| <key>NSPrivacyCollectedDataTypeTracking</key> | ||
| <false/> | ||
| <key>NSPrivacyCollectedDataTypePurposes</key> | ||
| <array> | ||
| <string>NSPrivacyCollectedDataTypePurposeAnalytics</string> | ||
| </array> | ||
| </dict> | ||
| </array> | ||
|
|
||
| <!-- Required-reason API declarations for first-party usage only. --> | ||
| <key>NSPrivacyAccessedAPITypes</key> | ||
| <array> | ||
| <dict> | ||
| <!-- App preferences, onboarding state, push-registration opt-in, and | ||
| device-registry persistence via UserDefaults / @AppStorage. --> | ||
| <key>NSPrivacyAccessedAPIType</key> | ||
| <string>NSPrivacyAccessedAPICategoryUserDefaults</string> | ||
| <key>NSPrivacyAccessedAPITypeReasons</key> | ||
| <array> | ||
| <string>CA92.1</string> | ||
| </array> | ||
| </dict> | ||
| <dict> | ||
| <!-- Reads the app's own bundle executable modification date for the | ||
| in-app diagnostic build stamp. --> | ||
| <key>NSPrivacyAccessedAPIType</key> | ||
| <string>NSPrivacyAccessedAPICategoryFileTimestamp</string> | ||
| <key>NSPrivacyAccessedAPITypeReasons</key> | ||
| <array> | ||
| <string>DDA9.1</string> | ||
| </array> | ||
| </dict> | ||
| </array> | ||
| </dict> | ||
| </plist> |
Uh oh!
There was an error while loading. Please reload this page.