-
Notifications
You must be signed in to change notification settings - Fork 1.2k
Sign a Simulator into WordPress.com from a launch-argument token #25801
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
base: trunk
Are you sure you want to change the base?
Changes from all commits
7ee3a19
4ea87bd
6657667
ae140b0
976ef69
e3dbe48
42723ad
22b1cf3
acb2c08
0a980fe
ef2cc96
c1fffa3
6a719d5
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 |
|---|---|---|
| @@ -1,7 +1,10 @@ | ||
| .PHONY: help dependencies | ||
| .PHONY: help dependencies sim-login | ||
|
|
||
| help: ## Show available targets | ||
| @grep -E '^[a-zA-Z_-]+:.*##' $(MAKEFILE_LIST) | awk -F ':.*## ' '{printf " %-20s %s\n", $$1, $$2}' | ||
|
|
||
| dependencies: ## Download and cache Gutenberg XCFrameworks | ||
| ./Scripts/download-gutenberg-xcframeworks.sh | ||
|
|
||
| sim-login: ## Sign an iOS Simulator into WordPress.com (vars: DEVICE, APP, RESET=1; token from ~/.wpcom-token) | ||
| ./Scripts/sim-signin.sh $(if $(APP),--app $(APP)) $(if $(DEVICE),--device $(DEVICE)) $(if $(RESET),--reset) $(ARGS) |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,191 @@ | ||
| #!/bin/bash | ||
| # | ||
| # Sign an iOS Simulator into WordPress.com with a bearer token, in one command. | ||
| # | ||
| # Wraps `xcrun simctl launch`, passing the `-wpcom-token` launch argument the app reads on | ||
| # the login screen to finish WordPress.com sign-in automatically — no taps required. | ||
| # Build and install the app on the simulator first (e.g. from Xcode). | ||
|
|
||
| set -euo pipefail | ||
|
|
||
| usage() { | ||
| cat <<'EOF' | ||
| Sign an iOS Simulator into WordPress.com with a bearer token, in one command. | ||
|
|
||
| Usage: | ||
| Scripts/sim-signin.sh [options] | ||
|
|
||
| Options: | ||
| -a, --app <jetpack|wordpress> App to sign in (default: jetpack) | ||
| -d, --device <udid|name> Target simulator (default: the running one; prompts if several) | ||
| -r, --reset Uninstall and reinstall the app first, for a clean slate | ||
| -h, --help Show this help | ||
|
|
||
| Examples: | ||
| Scripts/sim-signin.sh # Jetpack, booted simulator | ||
| Scripts/sim-signin.sh --app wordpress # WordPress | ||
| Scripts/sim-signin.sh --reset # reinstall for a clean slate first | ||
|
|
||
| The WordPress.com bearer token is read from (in order): | ||
| 1. WPCOM_TOKEN environment variable | ||
| 2. ~/.wpcom-token file | ||
| 3. otherwise the script prompts you to paste one (and offers to save it to ~/.wpcom-token) | ||
|
|
||
| It is deliberately NOT accepted as a command-line flag: a token passed on the command line | ||
| would be saved in your shell history. Set WPCOM_TOKEN or write ~/.wpcom-token once. | ||
| EOF | ||
| } | ||
|
|
||
| app="jetpack" | ||
| device="" | ||
| reset=false | ||
| token="" | ||
|
|
||
| require_value() { | ||
| # $1 = option name, $2 = remaining argument count ($#) | ||
| if [[ "$2" -lt 2 ]]; then | ||
| echo "error: $1 requires a value" >&2 | ||
| exit 1 | ||
| fi | ||
| } | ||
|
|
||
| resolve_device() { | ||
| # Set `device` to a booted simulator: the only one if just one is booted, otherwise | ||
| # prompt to choose. Errors if none are booted. Called only when --device was omitted. | ||
| local udids=() names=() line udid name | ||
| while IFS= read -r line; do | ||
| # `|| true` so a UUID-less line (or a SIGPIPE from `head` under `pipefail`) doesn't | ||
| # trip `set -e` and abort the whole script before the `continue` below can skip it. | ||
| udid=$(printf '%s\n' "$line" | grep -oiE '[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}' | head -1 || true) | ||
| [[ -z "$udid" ]] && continue | ||
| name=$(printf '%s\n' "$line" | sed -E 's/^[[:space:]]*//; s/[[:space:]]*\([0-9A-Fa-f-]{36}\).*$//') | ||
| udids+=("$udid") | ||
| names+=("$name") | ||
| done < <(xcrun simctl list devices booted | grep -F "(Booted)") | ||
|
|
||
| local n=${#udids[@]} | ||
| if [[ "$n" -eq 0 ]]; then | ||
| echo "error: no booted simulator. Boot one (open Simulator, or 'xcrun simctl boot <udid>'), or pass --device <udid>." >&2 | ||
| exit 1 | ||
| fi | ||
| if [[ "$n" -eq 1 ]]; then | ||
| device="${udids[0]}" | ||
| echo "Using the only booted simulator: ${names[0]} (${device})" | ||
| return | ||
| fi | ||
|
|
||
| echo "Multiple simulators are booted — choose one:" >&2 | ||
| local i | ||
| for (( i = 0; i < n; i++ )); do | ||
| printf " %2d) %s (%s)\n" "$(( i + 1 ))" "${names[i]}" "${udids[i]}" >&2 | ||
| done | ||
| local sel | ||
| while true; do | ||
| printf "Select a simulator [1-%d]: " "$n" >&2 | ||
| if ! read -r sel; then | ||
| echo >&2 | ||
| echo "error: no selection made; re-run with --device <udid>." >&2 | ||
| exit 1 | ||
| fi | ||
| if [[ "$sel" =~ ^[0-9]+$ ]] && [[ "$sel" -ge 1 ]] && [[ "$sel" -le "$n" ]]; then | ||
| device="${udids[sel - 1]}" | ||
| echo "Using ${names[sel - 1]} (${device})" | ||
| return | ||
| fi | ||
| echo " not a valid choice: '$sel'" >&2 | ||
| done | ||
| } | ||
|
|
||
| reset_app() { | ||
| # A clean slate: uninstall the app (which removes its entire data container — Core Data, | ||
| # UserDefaults, caches, cookies) and reinstall the same bundle. Unlike the in-app | ||
| # `-ui-test-reset-everything` wipe, `simctl install` is synchronous, so there's no window to | ||
| # race before the sign-in launch, and it clears more than just Core Data + UserDefaults. | ||
| local app_bundle bundle_name | ||
| app_bundle=$(xcrun simctl get_app_container "$device" "$bundle_id" app 2>/dev/null || true) | ||
| if [[ -z "$app_bundle" || ! -d "$app_bundle" ]]; then | ||
| echo "error: can't reset — $app ($bundle_id) isn't installed on '$device'. Build and install it first (e.g. from Xcode)." >&2 | ||
| exit 1 | ||
| fi | ||
| bundle_name=$(basename "$app_bundle") | ||
|
|
||
| # Uninstall deletes the installed bundle in place, so stage a copy to reinstall from. | ||
| # `reset_staging` is intentionally global so the EXIT trap can clean it up at script exit. | ||
| reset_staging=$(mktemp -d) | ||
| trap 'rm -rf "$reset_staging"' EXIT | ||
| cp -R "$app_bundle" "$reset_staging/$bundle_name" | ||
|
|
||
| echo "Resetting $app (uninstall + reinstall for a clean slate)…" | ||
| xcrun simctl uninstall "$device" "$bundle_id" | ||
| xcrun simctl install "$device" "$reset_staging/$bundle_name" | ||
| } | ||
|
|
||
| while [[ $# -gt 0 ]]; do | ||
| case "$1" in | ||
| -a|--app) require_value "$1" "$#"; app="$2"; shift 2 ;; | ||
| -d|--device) require_value "$1" "$#"; device="$2"; shift 2 ;; | ||
| -r|--reset) reset=true; shift ;; | ||
| -h|--help) usage; exit 0 ;; | ||
| *) echo "error: unknown argument '$1'" >&2; usage >&2; exit 1 ;; | ||
| esac | ||
| done | ||
|
|
||
| # The token is read from the WPCOM_TOKEN environment variable, then a ~/.wpcom-token file. | ||
| # It is intentionally not a command-line argument, to keep it out of shell history. | ||
| if [[ -z "$token" ]]; then | ||
| token="${WPCOM_TOKEN:-}" | ||
| fi | ||
| if [[ -z "$token" && -f "$HOME/.wpcom-token" ]]; then | ||
| token="$(tr -d '[:space:]' < "$HOME/.wpcom-token")" | ||
| fi | ||
|
|
||
| # Nothing found anywhere — prompt for one. Input is hidden, since it's a secret. | ||
| if [[ -z "$token" ]]; then | ||
| printf "No WordPress.com token found (WPCOM_TOKEN / ~/.wpcom-token).\n" >&2 | ||
| printf "Paste a bearer token (hidden), or press Return to cancel: " >&2 | ||
| read -rs token || true | ||
| printf "\n" >&2 | ||
| token="$(printf '%s' "$token" | tr -d '[:space:]')" | ||
| if [[ -n "$token" ]]; then | ||
| printf "Token received (%s chars).\n" "${#token}" >&2 | ||
| printf "Save it to ~/.wpcom-token for next time? [y/N] " >&2 | ||
| read -r save_reply || true | ||
| if [[ "$save_reply" =~ ^[Yy]$ ]]; then | ||
| token_file="$HOME/.wpcom-token" | ||
| # umask keeps the new file owner-only; chmod covers an existing, looser file. | ||
| if (umask 077; printf '%s\n' "$token" > "$token_file"); then | ||
| chmod 600 "$token_file" 2>/dev/null || true | ||
| printf "Saved to %s (mode 600).\n" "$token_file" >&2 | ||
| else | ||
| printf "warning: could not write %s; continuing without saving.\n" "$token_file" >&2 | ||
| fi | ||
| fi | ||
| fi | ||
| fi | ||
|
|
||
| if [[ -z "$token" ]]; then | ||
| echo "error: no token — set WPCOM_TOKEN, write ~/.wpcom-token, or paste one when prompted" >&2 | ||
| usage >&2 | ||
| exit 1 | ||
| fi | ||
|
|
||
| case "$app" in | ||
| jetpack) bundle_id="com.automattic.jetpack" ;; | ||
| wordpress) bundle_id="org.wordpress" ;; | ||
| *) echo "error: unknown app '$app' (expected 'jetpack' or 'wordpress')" >&2; exit 1 ;; | ||
| esac | ||
|
|
||
| # When no --device was given, target the running simulator (prompting if several are booted). | ||
| if [[ -z "$device" ]]; then | ||
| resolve_device | ||
| fi | ||
|
|
||
| echo "Signing $app ($bundle_id) into WordPress.com on simulator '$device'…" | ||
|
|
||
| if [[ "$reset" == true ]]; then | ||
| reset_app | ||
| fi | ||
|
|
||
| xcrun simctl launch --terminate-running-process "$device" "$bundle_id" -wpcom-token "$token" | ||
|
|
||
| echo "Done. The app signs in automatically from the login screen." |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -63,6 +63,10 @@ public class WordPressAppDelegate: UIResponder, UIApplicationDelegate { | |
| private let remoteFeatureFlagStore = RemoteFeatureFlagStore() | ||
| private let remoteConfigStore = RemoteConfigStore() | ||
|
|
||
| /// Limits the `-wpcom-token` launch-argument sign-in to a single attempt per process, | ||
| /// so signing out (which returns to the login screen) doesn't immediately re-sign in. | ||
| private var didAttemptLaunchArgumentSignIn = false | ||
|
|
||
| private var mainContext: NSManagedObjectContext { | ||
| ContextManager.shared.mainContext | ||
| } | ||
|
|
@@ -901,4 +905,36 @@ extension WordPressAppDelegate { | |
| } | ||
| ) | ||
| } | ||
|
|
||
| /// Completes WordPress.com sign-in automatically when a bearer token is supplied via the | ||
| /// `-wpcom-token` launch argument, so a Simulator can be signed in with a single | ||
| /// `simctl launch` and no taps on the login screen. | ||
| /// | ||
| /// No-op when the argument is absent, a WordPress.com account is already signed in, or a | ||
| /// launch-argument sign-in was already attempted this process. The last case matters because | ||
| /// signing out returns to the login screen — without it, the still-present token would | ||
| /// immediately sign the account back in. | ||
| func autoSignInWPComAccountFromLaunchArgumentIfNeeded() { | ||
| guard WordPressDotComAuthenticator.launchArgumentToken != nil else { | ||
| return | ||
| } | ||
| guard !didAttemptLaunchArgumentSignIn else { | ||
| return | ||
| } | ||
| guard (try? WPAccount.lookupDefaultWordPressComAccount(in: ContextManager.shared.mainContext)) == nil else { | ||
| return | ||
| } | ||
| guard let presenter = window?.topmostPresentedViewController else { | ||
| return | ||
| } | ||
| didAttemptLaunchArgumentSignIn = true | ||
| Task { @MainActor in | ||
| guard await WordPressDotComAuthenticator().signIn(from: presenter, context: .default) != nil else { | ||
| return | ||
| } | ||
| // Creating the account isn't enough on its own — without this the app stays on the | ||
| // login screen. Swap the window root to the signed-in app (no epilogue, so no taps). | ||
| windowManager.showUI() | ||
| } | ||
| } | ||
|
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. I intentionally kept the sign in flow manual, mainly because I want to avoid introducing another code path to the sign in flow, like this function here. The current auto-sign flow only changes where to get the token/credentials (web or env var). If we want to fully automated sign in, do you think we can move the logic to the view controller that shows the buttons? Something like: For self-hosted site auto-sign in, we can do similar, something like With this direction, the sign in code path stays the same, manually or fully automated. BTW, the |
||
| } | ||
Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There may be multiple running simulators. Or, maybe I want a new one.
I feel like this belongs to personal configuration. What do you think?
Overall, I feel like the new script adds too many restrictions to what the agent can do intuitively. Currently, I can say "Create a new simulator, sign in to the test account (WP.com or a self-hosted site), perform this end-to-end verification on the Jetpack app", and the agent has enough info (from the AGENTS.md and the sign in doc) to help with that. With this new change, the agent can't get how to do that from the prompts themselves. I have not tried the new script, but I suspect the agent would read the makefile and the new script to understand what it does and then perform the actions.
The new script wraps an oneline
simctlcommand, which AI agents should be able to see in the docs/simulator-sign-in.md. Maybe it solves other problems that I don't see?