Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
39 commits
Select commit Hold shift + click to select a range
e88243b
ci(ios): build an App Store package on a macOS runner
kipavy Oct 24, 2026
a47e61f
ci(ios): install the provisioning profile where Xcode 16+ looks for it
kipavy Oct 24, 2026
ebdd0c7
ci(ios): print the signing settings when the build fails
kipavy Oct 24, 2026
4777c69
ci(ios): sign at export time with the App Store Connect API key
kipavy Oct 24, 2026
0e5941a
ci(ios): fix the API key line
kipavy Oct 24, 2026
ab0a4ef
feat(ios): Face ID unlock and data-protection keychain
kipavy Oct 24, 2026
24d1592
fix(ios): skip serial port enumeration (IOKit is not available)
kipavy Oct 24, 2026
5efde33
ci(ios): run the app in the simulator and keep screenshots
kipavy Oct 24, 2026
4e4fb85
ci(ios): TestFlight upload on demand, run number as build number
kipavy Oct 25, 2026
9050f25
feat(ios): save SFTP downloads to the Files app
kipavy Oct 25, 2026
69bf653
ci(ios): drive the app in the simulator end to end
kipavy Oct 25, 2026
d4bdd19
feat(ios): render the mobile shell on iOS
kipavy Oct 25, 2026
305f83b
Merge remote-tracking branch 'origin/ios/mobile-shell' into ci/ios
kipavy Oct 25, 2026
1a5005c
Merge remote-tracking branch 'origin/ci/ios' into ios/e2e
kipavy Oct 25, 2026
9914656
ci(ios-e2e): follow the mobile shell (FAB, autosaving editor, panels)
kipavy Oct 25, 2026
453ce9e
feat(ios): native share sheet for SFTP files and haptic feedback
kipavy Oct 25, 2026
9db9b8a
ci(ios): App Store Connect API workflow (status)
kipavy Oct 25, 2026
6fadf95
ci(ios): cancel superseded push builds
kipavy Oct 25, 2026
b298ee5
ci(ios): fill the App Store listing through the API
kipavy Oct 25, 2026
d487f00
ci(ios-e2e): release build with tauri/devtools (debug does not link)
kipavy Oct 25, 2026
7014646
ci(ios-e2e): reset page scroll before taps, check the editor closed
kipavy Oct 25, 2026
7277532
ci(ios-e2e): PTY for exec requests, taps follow the zoomed visual vie…
kipavy Oct 25, 2026
a3056fd
ci(ios-e2e): JS-click fallback when a native tap misses, flagged in t…
kipavy Oct 25, 2026
7fa673b
ci(ios-e2e): hide the keyboard before connecting, fix the editor-clos…
kipavy Oct 25, 2026
ddc5c85
ci(ios-e2e): keep reading input across resizes, type with the system …
kipavy Oct 25, 2026
87a9951
ci(ios-e2e): type the command by tapping the iOS keyboard keys
kipavy Oct 25, 2026
89001cb
feat(ios): hide the keyboard accessory bar, target iPhone only
kipavy Oct 25, 2026
c597af6
Merge remote-tracking branch 'origin/ios/e2e' into ci/ios
kipavy Oct 25, 2026
1b72ac2
ci(ios-e2e): App Store screenshot mode with demo data
kipavy Oct 25, 2026
0d8b145
ci(ios-e2e): js-click fallback for Get started, longer async script t…
kipavy Oct 25, 2026
ad3c9e1
ci(ios-e2e): time out hung steps, keep a report if the job is killed
kipavy Oct 25, 2026
ad897e7
fix(ios): stop the focus zoom on inputs; screenshot run reconnects
kipavy Oct 25, 2026
17971d0
ci(ios-e2e): store screenshots follow the app flow, demo folder in /U…
kipavy Oct 25, 2026
3cc2e04
ci(ios-e2e): time out simulator screenshots (a hung simctl froze the …
kipavy Oct 25, 2026
237c171
ci(ios-e2e): store screenshots at 1206x2622 (iPhone Pro)
kipavy Oct 25, 2026
ab71e70
Merge dev (v0.64.1) into ci/ios
kipavy Oct 28, 2026
65d5b06
Merge dev into ci/ios
kipavy Oct 28, 2026
1f7d063
fix(ios): ship the Voltius app icon instead of Tauri's placeholder
kipavy Oct 28, 2026
c978fa4
ci(ios): submit every release for App Store review (#722)
kipavy Oct 28, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
40 changes: 40 additions & 0 deletions .github/workflows/asc.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
# Reads or updates the App Store Connect record of the iOS app (Voltius SSH,
# bundle id app.voltius) through the App Store Connect API. Nothing here
# submits the app for review.
name: asc

on:
# A push only prints the state (and registers the workflow for dispatch).
push:
branches: [ci/ios]
paths: ["scripts/asc/**", ".github/workflows/asc.yml"]
workflow_dispatch:
inputs:
task:
description: "Script in scripts/asc/ to run"
required: true
default: status
type: choice
options: [status, listing]

permissions:
contents: read

jobs:
run:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v7

- name: Setup node
uses: actions/setup-node@v7
with:
node-version: lts/*

- name: Run scripts/asc/${{ inputs.task || 'status' }}.mjs
env:
APPLE_API_KEY: ${{ secrets.APPLE_API_KEY }}
APPLE_API_ISSUER: ${{ secrets.APPLE_API_ISSUER }}
APPLE_API_KEY_P8: ${{ secrets.APPLE_API_KEY_P8 }}
run: node scripts/asc/${{ inputs.task || 'status' }}.mjs
184 changes: 184 additions & 0 deletions .github/workflows/ios-e2e.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,184 @@
# Drives the iOS app in the simulator, step by step, since nobody on the team
# has an iPhone: Appium (XCUITest) taps the screen and reads the WebView DOM,
# a throwaway SSH/SFTP server on the runner plays the remote host.
#
# Every step leaves a screenshot and a DOM dump in the voltius-ios-e2e
# artifact; summary.md lists which steps passed. A failing app step does not
# fail the job (the point is the report); a broken harness does.
#
# The build is a release build with the tauri/devtools feature, which makes the
# WKWebView inspectable (Appium needs it for the WEBVIEW context). A debug build
# does not link on iOS: without dead-code stripping, serialport pulls in IOKit.
name: ios-e2e

on:
push:
branches: [ios/e2e, ci/ios]
workflow_dispatch:
inputs:
ref:
description: "Ref to build"
required: true
type: string
store:
description: "App Store screenshots (iPhone 6.9-inch, demo data) instead of the test report"
required: false
default: false
type: boolean

permissions:
contents: read

jobs:
simulator-e2e:
runs-on: macos-latest
timeout-minutes: 75
env:
OUT_DIR: ${{ github.workspace }}/e2e-out
SSH_PORT: "2222"
SSH_USER: ${{ inputs.store && 'deploy' || 'e2e' }}
STORE: ${{ inputs.store && '1' || '0' }}
steps:
- name: Checkout repository
uses: actions/checkout@v7
with:
ref: ${{ inputs.ref || github.ref }}

- name: Setup pnpm
uses: pnpm/action-setup@v6

- name: Setup node
uses: actions/setup-node@v7
with:
node-version: lts/*
cache: "pnpm"

- name: Read toolchain channel from rust-toolchain.toml
id: rust
run: echo "channel=$(sed -nE "s/^channel[[:space:]]*=[[:space:]]*\"([^\"]+)\".*/\1/p" rust-toolchain.toml)" >> "$GITHUB_OUTPUT"

- name: Install Rust toolchain
uses: dtolnay/rust-toolchain@master
with:
toolchain: ${{ steps.rust.outputs.channel }}
targets: aarch64-apple-ios-sim

- name: Rust cache
uses: swatinem/rust-cache@v2
with:
key: aarch64-apple-ios-sim-devtools
workspaces: ". -> target"

- name: Install frontend dependencies
run: pnpm install

- name: Generate the Xcode project
run: pnpm tauri ios init --ci

- name: Use the Voltius app icons
run: bash scripts/ios/use-app-icons.sh

- name: Target iPhone only
run: perl -pi -e 's/TARGETED_DEVICE_FAMILY = "?1,2"?;/TARGETED_DEVICE_FAMILY = 1;/' src-tauri/gen/apple/voltius.xcodeproj/project.pbxproj

- name: Build for the simulator (inspectable WebView)
env:
VOLTIUS_REQUIRE_SEEDED_PLUGINS: "1"
VITE_DISTRIBUTION: appstore
VITE_DEMO_SEED: ${{ inputs.store && '1' || '' }}
run: pnpm tauri ios build --target aarch64-sim --ci --features tauri/devtools

- name: Install Appium and the XCUITest driver
run: |
npm install --global appium@3
appium driver install xcuitest
npm install --prefix scripts/ios-e2e --no-package-lock --no-audit --no-fund

# Store run: the remote shell and SFTP open in a small demo project.
- name: Make the demo folder
if: inputs.store
run: |
demo=/Users/deploy
sudo mkdir -p "$demo" && sudo chown "$(id -u):$(id -g)" "$demo"
mkdir -p "$demo"/{app/src,backups,logs,www}
printf 'services:\n app:\n image: ghcr.io/acme/app:2.4.1\n ports: ["8080:8080"]\n db:\n image: postgres:17\n' > "$demo/docker-compose.yml"
printf 'server {\n listen 443 ssl;\n server_name app.example.com;\n location / { proxy_pass http://127.0.0.1:8080; }\n}\n' > "$demo/nginx.conf"
printf '#!/bin/sh\nset -e\ndocker compose pull && docker compose up -d\n' > "$demo/deploy.sh"; chmod +x "$demo/deploy.sh"
printf '# app\n\nProduction deployment.\n' > "$demo/README.md"
for d in 21 22 23 24 25; do head -c 3000000 /dev/urandom > "$demo/backups/db-2026-10-$d.sql.gz"; done
{
echo "2026-10-25 08:59:58 pulling ghcr.io/acme/app:2.4.1"
echo "2026-10-25 09:00:04 app | migrations: 3 applied"
echo "2026-10-25 09:00:05 app | listening on :8080"
echo "2026-10-25 09:00:06 health check passed (42 ms)"
echo "2026-10-25 09:00:06 deploy 2.4.1 done in 8.1 s"
echo "2026-10-25 09:00:07 old containers removed"
} > "$demo/logs/deploy.log"
echo "DEMO_DIR=$demo" >> "$GITHUB_ENV"

- name: Start the test SSH server
run: |
[ -z "$DEMO_DIR" ] || cd "$DEMO_DIR"
python3 -m venv "$RUNNER_TEMP/sshvenv"
"$RUNNER_TEMP/sshvenv/bin/pip" install --quiet asyncssh
password="e2e-$(openssl rand -hex 8)"
echo "SSH_PASSWORD=$password" >> "$GITHUB_ENV"
mkdir -p "$OUT_DIR/ssh"
E2E_SSH_USER="$SSH_USER" E2E_SSH_PASSWORD="$password" \
nohup "$RUNNER_TEMP/sshvenv/bin/python" "$GITHUB_WORKSPACE/scripts/ios-e2e/ssh_server.py" "$SSH_PORT" "$OUT_DIR/ssh" \
> "$OUT_DIR/ssh/server-stdout.log" 2>&1 &
for _ in $(seq 1 20); do nc -z 127.0.0.1 "$SSH_PORT" && break; sleep 1; done
nc -z 127.0.0.1 "$SSH_PORT"

- name: Boot a simulator
run: |
# App Store Connect takes its iPhone screenshots at 1206x2622 (the
# medium Dynamic Island iPhone, i.e. the Pro): use the Pro either way.
want_max=False
device=$(xcrun simctl list devices available -j | python3 -c "import json,sys; d=json.load(sys.stdin)['devices']; m=$want_max; print(next(x['udid'] for r in sorted(d, reverse=True) if 'iOS' in r for x in d[r] if x['name'].startswith('iPhone') and 'Pro' in x['name'] and ('Max' in x['name']) == m))")
echo "UDID=$device" >> "$GITHUB_ENV"
xcrun simctl boot "$device"
xcrun simctl bootstatus "$device"
if [ "$STORE" = 1 ]; then
xcrun simctl status_bar "$device" override --time 9:41 --dataNetwork wifi --wifiMode active --wifiBars 3 --cellularMode active --cellularBars 4 --batteryState charged --batteryLevel 100
fi
app=$(find src-tauri/gen/apple/build -name "*.app" -maxdepth 3 | head -1)
echo "APP_PATH=$GITHUB_WORKSPACE/$app" >> "$GITHUB_ENV"
echo "device $device, app $app"

- name: Start Appium
run: |
nohup appium --address 127.0.0.1 --port 4723 --log "$OUT_DIR/appium.log" --log-no-colors > /dev/null 2>&1 &
for _ in $(seq 1 60); do curl -fs http://127.0.0.1:4723/status > /dev/null && break; sleep 1; done
curl -fs http://127.0.0.1:4723/status

- name: Drive the app
timeout-minutes: 30
env:
SSH_LOG_DIR: ${{ github.workspace }}/e2e-out/ssh
run: node scripts/ios-e2e/run.mjs

- name: Collect the app logs
if: always()
run: |
[ -n "$UDID" ] || exit 0
xcrun simctl spawn "$UDID" log show --last 60m --style compact \
--predicate 'process == "Voltius"' > "$OUT_DIR/app-system.log" 2>/dev/null || true
grep -i -E "error|fault|panic|voltius_lib|tauri|\[voltius" "$OUT_DIR/app-system.log" | grep -v -i -E "BoardServices|xctest|KeyboardArbiter" | tail -400 > "$OUT_DIR/app-errors.log" || true

- name: Upload screenshots and logs
if: always()
uses: actions/upload-artifact@v7
with:
name: voltius-ios-e2e
path: e2e-out/
retention-days: 14

- name: Upload the store screenshots
if: always() && inputs.store
uses: actions/upload-artifact@v7
with:
name: voltius-ios-store-screenshots
path: e2e-out/store/
if-no-files-found: ignore
retention-days: 30
Loading
Loading