Files
web-terminal/ios/project.yml
Yaojia Wang 5cc755b0b6 fix(ios,android): close the acceptance gaps the review found, add Android CI
- T-iOS-34's stated acceptance is "最大字号不破版" and it was failing: the key bar
  froze its height at 52pt while an AX5 keycap needs 108.24pt, so caps clipped —
  recorded as a withKnownIssue rather than fixed. Height now derives from the
  content size category (and tracks live changes via registerForTraitChanges);
  the known-issue marker is gone, replaced by positive assertions including a
  12-category no-clip sweep and a guard that stays red if anyone writes the
  constant back. Honest tradeoff: the keycap font is clamped at .accessibility2,
  the same policy the design system already applies to dense content, because an
  unclamped AX5 bar would eat the terminal. A test pins the clamp so the two
  cannot drift.

- Thumbnails silently 401'd on a token-gated host: the pipeline built its own
  transport with no token source, and by design never throws, so every preview
  degraded to a placeholder with no signal. Assembled from AppEnvironment now.

- Android had zero CI while the token wave shipped 24 files of secret-handling
  code. The instrumented leg is workflow_dispatch-only and says why in the file:
  no one has ever seen it green on a runner, and a required leg nobody trusts
  just produces a false green.

- Android persisted a validated token before the host probe succeeded, stranding
  a secret for a host that never paired.

App bundle 534 -> 550 on both simulators, zero known issues. Android 687 -> 691.
2026-07-30 16:46:20 +02:00

237 lines
11 KiB
YAML

# XcodeGen project spec — regenerate with: cd ios && xcodegen generate
# The generated WebTerm.xcodeproj is NOT committed (see ios/.gitignore).
#
# TOOLCHAIN DEVIATION (recorded per plan §7 T-iOS-1): the plan names
# "default MainActor isolation" (SWIFT_DEFAULT_ACTOR_ISOLATION), which is
# Swift 6.2+/Xcode 26 only. On Swift 6.1 / Xcode 16.3 the closest supported
# equivalent is Swift 6 language mode + strict concurrency, with explicit
# @MainActor annotations where isolation is required.
name: WebTerm
options:
bundleIdPrefix: com.yaojia
deploymentTarget:
iOS: "17.0"
createIntermediateGroups: true
settings:
base:
SWIFT_VERSION: "6.0" # Swift 6 language mode
SWIFT_STRICT_CONCURRENCY: complete
IPHONEOS_DEPLOYMENT_TARGET: "17.0"
TARGETED_DEVICE_FAMILY: "1,2" # iPhone + iPad (T-iPad-1; adaptive layout)
CODE_SIGN_STYLE: Automatic
# Free personal Apple team (O="Yaojia Wang"). Required for ANY device build:
# without it xcodebuild stops at 'Signing for "WebTerm" requires a
# development team'. Not a secret — a Team ID is public in every signed ipa.
DEVELOPMENT_TEAM: C738Z66SRW
# Default for the opt-in push-entitlements switch (A1). Empty ⇒ the target's
# CODE_SIGN_ENTITLEMENTS = "${WEBTERM_PUSH_ENTITLEMENTS}" resolves to nothing
# and no entitlements file is signed in. Declaring the default HERE (rather
# than relying on the setting being undefined) keeps the switch strictly
# generate-time: a project-level build setting outranks the build-time
# process environment, so an exported shell var can never silently attach
# entitlements to a build generated without it.
WEBTERM_PUSH_ENTITLEMENTS: ""
packages:
WireProtocol:
path: Packages/WireProtocol
SessionCore:
path: Packages/SessionCore
HostRegistry:
path: Packages/HostRegistry
APIClient:
path: Packages/APIClient
ClientTLS:
path: Packages/ClientTLS # C-iOS · device client-cert (mTLS) leaf package
TestSupport:
path: Packages/TestSupport # test doubles — WebTermTests only, never the app target
# SwiftTerm is the ONLY third-party dependency, attached to the App target
# ONLY (plan §2) — packages must never import it.
#
# PINNED, not floating (`from:`). The generated .xcodeproj — and with it
# Package.resolved, which lives inside the bundle — is gitignored, so every
# agent/CI run re-resolves from scratch, and a floating `from:` would silently
# drift to whatever is newest. An exact pin makes every agent/CI run resolve
# the SAME source (an unattended drift off `from: 1.13.0` is what once broke
# the App target — see the history below); raising it is a deliberate,
# verified edit, and 1.15.0 below IS that deliberate raise.
#
# History (T-iOS-33/31 root fix): v1.14.0 added `public var hasActiveSelection`
# to iOSTerminalView, which collided with a same-named property the App
# declared on its `TerminalView` subclass — and `override` cannot fix it,
# since upstream's is `public`, not `open`. The fix was to DROP the local
# property and use upstream's (identical semantics: `selection?.active ??
# false`, vs. the local `canPerformAction(copy)` which SwiftTerm answers from
# the same flag). Done in TerminalScreen.swift, so the pin now rides at
# 1.15.0, which also brings `searchMatchSummary` (a "2/14" match counter the
# find bar can adopt later — deliberately not consumed yet, so the pin stays
# revertible).
SwiftTerm:
url: https://github.com/migueldeicaza/SwiftTerm
exactVersion: 1.15.0
targets:
WebTerm:
type: application
platform: iOS
sources:
# The .entitlements file lives next to the sources but must never be
# bundled as a resource — it is signing input, not app content.
- path: App/WebTerm
excludes:
- WebTerm.entitlements
dependencies:
- package: WireProtocol
- package: SessionCore
- package: HostRegistry
- package: APIClient
- package: ClientTLS
- package: SwiftTerm
settings:
base:
PRODUCT_BUNDLE_IDENTIFIER: com.yaojia.webterm
# XcodeGen's iOS-application preset injects Apple's DEPRECATED
# pre-Xcode-12 development identity string at target level (which
# outranks anything set in the project-level settings above), so it has
# to be overridden right here. "Apple Development" is the modern unified
# identity Xcode 12+ issues — it is what `security find-identity`
# actually holds, so the old string could only ever resolve by alias.
CODE_SIGN_IDENTITY: "Apple Development"
# OPT-IN push entitlements (frozen switch name WEBTERM_PUSH_ENTITLEMENTS).
# XcodeGen substitutes ${VAR} from the environment at `xcodegen generate`
# time when the var is SET, and leaves the literal placeholder when it is
# UNSET — the placeholder then resolves against the project-level
# WEBTERM_PUSH_ENTITLEMENTS: "" default, i.e. no entitlements.
# default (free team): xcodegen generate
# push on (paid team): WEBTERM_PUSH_ENTITLEMENTS=App/WebTerm/WebTerm.entitlements xcodegen generate
CODE_SIGN_ENTITLEMENTS: ${WEBTERM_PUSH_ENTITLEMENTS}
# Target-level on purpose (T-iOS-19 finding): XcodeGen's target platform
# default overrides a project-level value. iPad adaptation (T-iPad-1)
# opens this to iPhone + iPad; the adaptive layout (LayoutPolicy /
# NavigationSplitView, T-iPad-2) makes the iPad layout first-class.
TARGETED_DEVICE_FAMILY: "1,2"
info:
path: App/WebTerm/Resources/Info.plist
properties:
CFBundleDisplayName: WebTerm
UILaunchScreen: {}
# Deep-link scheme (P1 T-iOS-22): webterminal://open?host=<id>&join=<uuid>
# Registered here by the orchestrator (project.yml coordination point);
# all parsing/validation lives in DeepLinkRouter (untrusted input).
CFBundleURLTypes:
- CFBundleURLName: com.yaojia.webterm.deeplink
CFBundleURLSchemes: [webterminal]
# iPhone: portrait + both landscapes (no upside-down — matches P0).
UISupportedInterfaceOrientations:
- UIInterfaceOrientationPortrait
- UIInterfaceOrientationLandscapeLeft
- UIInterfaceOrientationLandscapeRight
# iPad (T-iPad-1): all four orientations — iPad users hold it any way,
# and Split View / Stage Manager assume full orientation freedom.
UISupportedInterfaceOrientations~ipad:
- UIInterfaceOrientationPortrait
- UIInterfaceOrientationPortraitUpsideDown
- UIInterfaceOrientationLandscapeLeft
- UIInterfaceOrientationLandscapeRight
# ── Security-critical keys, transcribed verbatim from PLAN_IOS_CLIENT §5.2 ──
# NO NSAllowsArbitraryLoads anywhere (release OR debug): the five CIDR
# exceptions below cover LAN + hotspot + Tailscale CGNAT + simulator
# loopback, so no arbitrary-loads escape hatch is needed in P0.
# T-iOS-19 re-verifies all five CIDR blocks in the release ipa.
NSAppTransportSecurity:
NSAllowsLocalNetworking: true # only covers .local / dotless hostnames, not bare IPs
NSExceptionDomains: # bare IPs use CIDR exceptions (iOS 17+ semantics)
"192.168.0.0/16":
NSExceptionAllowsInsecureHTTPLoads: true
"10.0.0.0/8":
NSExceptionAllowsInsecureHTTPLoads: true
"172.16.0.0/12": # RFC1918 third block (iPhone hotspot 172.20.10.x / corp LAN)
NSExceptionAllowsInsecureHTTPLoads: true
"100.64.0.0/10": # Tailscale CGNAT
NSExceptionAllowsInsecureHTTPLoads: true
"127.0.0.0/8": # simulator dev-loop (CIDR, not a single-IP key — DevForums 6205)
NSExceptionAllowsInsecureHTTPLoads: true
NSLocalNetworkUsageDescription: "连接你自己电脑上的 web-terminal 服务器"
NSCameraUsageDescription: "扫描 web 终端的配对二维码"
# Voice push-to-talk (T-iOS-31, P2). Both keys are mandatory before the
# feature ships: missing either one = TCC crash the moment PTT is pressed.
# Added here by A1 because project.yml has a single owner.
NSMicrophoneUsageDescription: "按住说话,把你的口述录成终端输入"
NSSpeechRecognitionUsageDescription: "把你的口述转成文字,你确认后才送进终端"
# Silent APNs wake so a hook notification can be processed without the
# app in the foreground (T-iOS-21). A background MODE is not an Apple
# capability, so it is unconditional and safe on the free team — unlike
# the aps-environment ENTITLEMENT, which stays opt-in (see above).
UIBackgroundModes:
- remote-notification
# Unit-test bundle for App-target logic (ViewModels & pure UI components,
# W3 T-iOS-11…14). Added by the orchestrator as a coordination point
# (project.yml is T-iOS-1's Owns; same precedent as the W1 manifest wiring).
# These tests exist for correctness, NOT for the coverage gate: the gate runs
# per SwiftPM package over that package's own sources
# (IntegrationTests/scripts/coverage-gate.sh), and an App-target test bundle is
# not one of them. It now covers FIVE packages — plan §9's four plus ClientTLS,
# which B4 added (see .github/workflows/ios.yml's package-tests matrix).
WebTermTests:
type: bundle.unit-test
platform: iOS
sources:
- App/WebTermTests
dependencies:
- target: WebTerm
- package: WireProtocol
- package: SessionCore
- package: HostRegistry
- package: APIClient
- package: ClientTLS
- package: TestSupport
settings:
base:
GENERATE_INFOPLIST_FILE: true
TARGETED_DEVICE_FAMILY: "1,2"
TEST_HOST: "$(BUILT_PRODUCTS_DIR)/WebTerm.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/WebTerm"
BUNDLE_LOADER: "$(TEST_HOST)"
# XCUITest — exactly ONE scripted happy path per plan §9 (T-iOS-18 finding):
# pair via manual entry against a live loopback server → attach → type →
# approve a held gate. Kept deliberately minimal (fragile-surface rule).
WebTermUITests:
type: bundle.ui-testing
platform: iOS
sources:
- App/WebTermUITests
dependencies:
- target: WebTerm
settings:
base:
GENERATE_INFOPLIST_FILE: true
TARGETED_DEVICE_FAMILY: "1,2"
TEST_TARGET_NAME: WebTerm
schemes:
WebTerm:
build:
targets:
WebTerm: all
WebTermTests: [test]
run:
config: Debug
test:
config: Debug
targets:
- WebTermTests
# Separate scheme so the slow UI pass never gates the fast unit loop; CI
# runs it as its own leg (see .github/workflows/ios.yml).
WebTermUITests:
build:
targets:
WebTerm: all
WebTermUITests: [test]
test:
config: Debug
targets:
- WebTermUITests