Use PocketShare in CI
A pipeline job can rent a real phone for the length of its tests: pocketshare run rents one,
makes it an adb device on a local port, runs your command with ANDROID_SERIAL pointing at it,
saves a screenshot and Logcat (and, if you ask, a screen recording) when the command ends, and
always ends the rental: when the command passes or fails, when it can’t start, when it runs
past --timeout, when the job is cancelled. You pay per minute, never more than the spending cap
you set.
pocketshare run --model "Pixel 9a" --max-spend 1.00 --wait 10m --timeout 30m \ --artifacts pocketshare-artifacts -- ./gradlew connectedDebugAndroidTestBefore you start
Section titled “Before you start”- A dedicated API key. On pocketshare.io, Account › API keys, create a key for the pipeline with only phones:read, rentals:read and rentals:write, and an expiry. Don’t use an agent key: agent rentals end after a few minutes without MCP tool calls, which a test run doesn’t make.
- Store it as a secret: a masked, protected CI/CD variable
POCKETSHARE_API_KEYin GitLab, a repository secret in GitHub. The CLI reads it from the environment, never prints it, and doesn’t pass it to your command (--keep-api-keydoes, if your tests callpocketshare). - Limit what a run can cost. Every rental has a spending cap (
--max-spend): billing ends it there. Your account’s daily and monthly spending limits (Account › Settings › Spending limits) cap all rentals together, so a misconfigured pipeline can’t run up a bill. - An image with adb, plus whatever your tests need (a JDK for Gradle, Node.js for Appium, Maestro). The CLI runs on Linux (glibc, x64 or arm64) and macOS; on GitHub’s Ubuntu runners adb is already installed.
pocketshare run
Section titled “pocketshare run”pocketshare run [<phone-id> | --model M --brand B --android V --max-price $/h] --max-spend $ [--wait 10m] [--timeout 30m] [--artifacts DIR] [--record] [--json] -- <command> [args…]| Option | What it does |
|---|---|
<phone-id> or --model, --brand, --android (or --min-api), --max-price |
Which phone: one by ID, or the cheapest available phone that matches. --model matches the model name exactly, ignoring case (“Pixel 9a” or “Google Pixel 9a”). |
--max-spend $ |
The rental’s spending cap, required. |
--wait 10m |
Keep looking for a matching phone for this long (90s, 10m, 1h); without it, exit 6 at once if none is free. |
--timeout 30m |
The command’s time limit: it is stopped (SIGTERM, then SIGKILL after 10 s) and run exits 124. |
--artifacts DIR |
Save screenshot.png, logcat.txt, pocketshare-run.json and rental-id there. |
--record |
Also record the screen while the command runs (screen-recording.mp4, or numbered 3-minute segments, up to an hour). Needs --artifacts. |
--json |
The CLI’s own lines are JSON events on stderr (rented, connected, command_started, artifact, stopped, done…). |
--port N, --adb PATH, --no-adb-connect, --any-key |
As for pocketshare connect. By default run adds the phone to adb (adb connect) and fails if it can’t. |
The command runs without a shell, with these variables added to the environment:
| Variable | Value |
|---|---|
ANDROID_SERIAL |
127.0.0.1:PORT, the phone in adb devices; adb, Gradle and most tools use it. |
ADB_SERIAL |
The same, for tools that read this name. |
POCKETSHARE_RENTAL_ID |
The rental. |
Use sh -c '…' (single quotes) when the command needs a shell or names the serial itself:
otherwise your CI shell expands $ANDROID_SERIAL before pocketshare run has set it.
The command’s output passes through unchanged on stdout and stderr; the CLI’s own lines go to
stderr, prefixed pocketshare:.
Exit codes
Section titled “Exit codes”run exits with your command’s exit code once it has run. Before that, and for what ends it
early, it uses the CLI’s codes:
| Code | Meaning |
|---|---|
| the command’s | The command ran to the end (0 if it passed). |
| 2 | Bad usage. |
| 3 | No API key, a revoked key, or a key without the scopes. |
| 4 | Credit or a spending limit: the rent was refused, or billing ended the rental while the command ran (its cap, the account’s spending limit, the credit or its longest duration). |
| 5 | PocketShare unreachable or busy; or the rental couldn’t be confirmed stopped after the command passed. |
| 6 | No matching phone became available within --wait. |
| 124 | The command ran longer than --timeout. |
| 127 | The command couldn’t be started (not found). |
| 130, 143 | Interrupted (SIGINT, SIGTERM): the command was stopped, then the rental. |
| 1 | Anything else, for example the phone’s rental ended for another reason. |
A second Ctrl-C or SIGTERM doesn’t skip ending the rental; a third quits at once.
What it saves
Section titled “What it saves”| File | What |
|---|---|
screenshot.png |
The screen when the command ended. |
logcat.txt |
The phone’s whole Logcat buffer (logcat -d, up to 32 MB). |
screen-recording.mp4 |
With --record. |
pocketshare-run.json |
Rental ID, phone, serial, outcome, exit code, start and end, minutes billed, charge and the files saved. |
rental-id |
Written as soon as the phone is rented, so a job’s cleanup step can stop it (below). |
Saving is best effort and never changes the exit code; if the rental has already ended (its cap), there is nothing left to save.
GitLab CI
Section titled “GitLab CI”Copy the template into your repository as
ci/pocketshare.gitlab-ci.yml and include it. Its hidden job .pocketshare installs the CLI before your script (checking the release
signature, below), puts it on the PATH, keeps $POCKETSHARE_ARTIFACTS as job artifacts, and
in after_script stops the rental if the job was killed before run could.
include: - local: ci/pocketshare.gitlab-ci.yml
variables: POCKETSHARE_CLI_VERSION: 0.4.0 # pin a version; latest by default
android-tests: extends: .pocketshare image: ghcr.io/cirruslabs/android-sdk:35 # any glibc image with adb and a JDK script: - ./gradlew assembleDebug assembleDebugAndroidTest - pocketshare run --model "Pixel 9a" --max-spend 1.00 --wait 10m --timeout 30m --artifacts "$POCKETSHARE_ARTIFACTS" -- ./gradlew connectedDebugAndroidTestGitHub Actions
Section titled “GitHub Actions”Copy the composite action into your repository as .github/actions/pocketshare-run/action.yml
(there is no published uses: pocketshare/… action; the file is below)
and use it in a job:
jobs: android-tests: runs-on: ubuntu-24.04 steps: - uses: actions/checkout@v4 - uses: actions/setup-java@v4 with: { distribution: temurin, java-version: '17' } - run: ./gradlew assembleDebug assembleDebugAndroidTest - uses: ./.github/actions/pocketshare-run with: api-key: ${{ secrets.POCKETSHARE_API_KEY }} model: Pixel 9a max-spend: '1.00' wait: 10m timeout: 30m command: ./gradlew connectedDebugAndroidTestThe action installs the CLI (signature-checked), runs the command with bash, stops the rental in
an always() step if the run was cut short, uploads the artifacts with
actions/upload-artifact, and sets the outputs exit-code and rental-id. Inputs: api-key,
command, max-spend (required); phone, model, brand, android, max-price, wait
(10m), timeout (30m), record, artifacts, upload-artifacts, cli-version.
Examples
Section titled “Examples”Gradle’s connected tests run on the device named by ANDROID_SERIAL:
./gradlew assembleDebug assembleDebugAndroidTestpocketshare run --model "Pixel 9a" --max-spend 1.00 --wait 10m --timeout 30m \ --artifacts pocketshare-artifacts -- ./gradlew connectedDebugAndroidTestOr with adb and am instrument directly:
pocketshare run --android 14 --max-spend 1.00 --artifacts pocketshare-artifacts -- sh -c ' adb install -r -g app/build/outputs/apk/debug/app-debug.apk && adb install -r -g app/build/outputs/apk/androidTest/debug/app-debug-androidTest.apk && adb shell am instrument -w -r com.example.app.test/androidx.test.runner.AndroidJUnitRunner'am instrument exits 0 even when tests fail; Gradle’s task (or parsing its output) is the
reliable pass/fail signal.
curl -fsSL "https://get.maestro.mobile.dev" | bash # or a pinned Maestro in your imagepocketshare run --model "Pixel 9a" --max-spend 1.00 --wait 10m --timeout 20m \ --artifacts pocketshare-artifacts --record -- sh -c ' adb install -r app/build/outputs/apk/debug/app-debug.apk && "$HOME/.maestro/bin/maestro" --device "$ANDROID_SERIAL" test --format junit \ --output pocketshare-artifacts/maestro-report.xml .maestro/'Appium’s UiAutomator2 driver installs its server on the phone over adb and reaches it with
adb forward, which the phone’s host allows. Start Appium inside the command, so it stops
with it:
npm install -g appium && appium driver install uiautomator2pocketshare run --brand google --android 14 --max-spend 1.00 --wait 10m --timeout 30m \ --artifacts pocketshare-artifacts -- sh -c ' appium --log pocketshare-artifacts/appium.log & npx wait-on tcp:127.0.0.1:4723 && npx wdio run wdio.conf.ts'In the test’s capabilities, name the rented phone:
capabilities: [{ platformName: 'Android', 'appium:automationName': 'UiAutomator2', 'appium:udid': process.env.ANDROID_SERIAL, 'appium:app': 'app/build/outputs/apk/debug/app-debug.apk',}],Other CI systems (or a script) install the CLI with the same checked download, then use
run:
export POCKETSHARE_API_KEY=… # from the CI system's secret storeexport POCKETSHARE_INSTALL_DIR="$PWD/.pocketshare/bin" PATH="$PWD/.pocketshare/bin:$PATH"sh install-pocketshare-cli.sh # the script from the templates belowpocketshare run --model "Pixel 9a" --max-spend 1.00 --artifacts out -- ./run-tests.shOn Windows runners, run batch files through cmd /c (-- cmd /c gradlew.bat connectedDebugAndroidTest); Windows stops the command forcefully, without SIGTERM.
How the CLI is checked
Section titled “How the CLI is checked”Both templates install the CLI with the same short script (ci/install-pocketshare-cli.sh):
- Download
release-manifest.jsonand its signature fromhttps://pocketshare.io/downloads/cli/<version or latest>/. - Check the signature with OpenSSL 3 against PocketShare’s release key, which the script
pins (
pocketshare-release-2026, keyoLkDjloA3xXQmioSxTrBuKFygyrt867Jrm3PNSyyLSE), and that the manifest is for thepocketshareCLI and the version asked for. - Download the executable for the runner and check its SHA-256 against the signed manifest.
- Only then install it (and check it has
run). Anything else stops the job and installs nothing.
Settings: POCKETSHARE_CLI_VERSION (default latest; pin x.y.z for reproducible pipelines),
POCKETSHARE_INSTALL_DIR, and OPENSSL for an OpenSSL 3 binary where openssl is older (on
macOS: OPENSSL=$(brew --prefix openssl@3)/bin/openssl).
Costs and safety
Section titled “Costs and safety”- Ending is guaranteed by the CLI and backed up by the job.
runends the rental on every path and retries the request; if it still can’t confirm, it says so and exits non-zero. The templates’ cleanup step stops any rental whosepocketshare-run.jsondoesn’t say it was stopped. If everything fails, billing still ends the rental at its cap. - Warnings before a cutoff. About a minute before the cap, a spending limit, the credit or
the longest rental ends it,
runprints a warning (anending_soonevent with--json). - The phone is only yours. The adb endpoint listens on 127.0.0.1 and accepts only the runner’s own adb key; the phone’s host applies the same service policy to it as to any rental, and the phone is wiped and checked after the rental.
- Mind the queue.
--waitholds the job while it waits for a phone; set the job’s own timeout to cover--waitplus--timeout.
The GitLab template
Section titled “The GitLab template”ci/pocketshare.gitlab-ci.yml in the PocketShare repository. The installer in it is
ci/install-pocketshare-cli.sh, inlined so the file works on its own (a test keeps the copies
identical).
# PocketShare in GitLab CI: rent a real Android phone for a job, run your tests against it# with adb, keep a screenshot, Logcat and (optionally) a screen recording, and always release# the phone. You pay per minute, never more than the spending cap you set.## Use it by extending the hidden job `.pocketshare` (docs: https://pocketshare.io/docs/ci/):## include:# - local: ci/pocketshare.gitlab-ci.yml # a copy of this file in your repository## android-tests:# extends: .pocketshare# image: ghcr.io/cirruslabs/android-sdk:35 # any glibc image with adb (and a JDK for Gradle)# script:# - ./gradlew assembleDebug assembleDebugAndroidTest# - pocketshare run --model "Pixel 9a" --max-spend 1.00 --wait 10m --timeout 30m# --artifacts "$POCKETSHARE_ARTIFACTS" -- ./gradlew connectedDebugAndroidTest## Set POCKETSHARE_API_KEY as a masked, protected CI/CD variable: a dedicated key with only the# phones:read, rentals:read and rentals:write scopes (pocketshare.io > Account > API keys).# `pocketshare run` stops the rental when the command ends, fails, times out or the job is# cancelled; after_script stops it too if the job was killed before `run` could.## Before the job's script, the CLI is downloaded from the public downloads and installed only# if the release manifest's Ed25519 signature matches PocketShare's release key (OpenSSL 3)# and the executable matches the manifest's SHA-256 (the script is ci/install-pocketshare-cli.sh).# Pin POCKETSHARE_CLI_VERSION to a version (x.y.z) for reproducible pipelines.
.pocketshare: variables: POCKETSHARE_CLI_VERSION: latest POCKETSHARE_INSTALL_DIR: $CI_PROJECT_DIR/.pocketshare/bin POCKETSHARE_ARTIFACTS: pocketshare-artifacts before_script: - | cat >"${TMPDIR:-/tmp}/install-pocketshare-cli.sh" <<'POCKETSHARE_INSTALLER' #!/bin/sh # Installs the pocketshare CLI for a CI job, after checking it against PocketShare's signed # release manifest: the manifest's Ed25519 signature with the pinned release key (OpenSSL 3), # then the executable's SHA-256 from that manifest. Nothing is installed if either fails. # # POCKETSHARE_CLI_VERSION x.y.z, or latest (default) # POCKETSHARE_INSTALL_DIR where to put `pocketshare` (default: $HOME/.local/bin) # POCKETSHARE_DOWNLOADS the downloads (default: https://pocketshare.io/downloads/cli) # OPENSSL an OpenSSL 3 binary (default: openssl; macOS: Homebrew openssl@3) # # Linux (glibc) on x64 or arm64 and macOS. The same script is inlined in # ci/pocketshare.gitlab-ci.yml and ci/github/pocketshare-run/action.yml (a test keeps the # copies identical). Documentation: https://pocketshare.io/docs/ci/ set -eu ps_downloads=${POCKETSHARE_DOWNLOADS:-https://pocketshare.io/downloads/cli} ps_version=${POCKETSHARE_CLI_VERSION:-latest} ps_dir=${POCKETSHARE_INSTALL_DIR:-$HOME/.local/bin} ps_openssl=${OPENSSL:-openssl} # PocketShare's release key, pocketshare-release-2026 (releases/keys/ in the repository). ps_key='-----BEGIN PUBLIC KEY----- MCowBQYDK2VwAyEAoLkDjloA3xXQmioSxTrBuKFygyrt867Jrm3PNSyyLSE= -----END PUBLIC KEY-----' ps_die() { printf 'pocketshare install: %s\n' "$*" >&2 exit 1 } case "$ps_version" in latest | [0-9]*.[0-9]*.[0-9]*) ;; *) ps_die "bad POCKETSHARE_CLI_VERSION $ps_version" ;; esac case "$("$ps_openssl" version 2>/dev/null)" in 'OpenSSL '[3-9]*) ;; *) ps_die "needs OpenSSL 3 to check the release signature (set OPENSSL on macOS: brew install openssl@3)" ;; esac case "$(uname -s)-$(uname -m)" in Linux-x86_64 | Linux-amd64) ps_file=pocketshare-linux-x64 ;; Linux-aarch64 | Linux-arm64) ps_file=pocketshare-linux-arm64 ;; Darwin-arm64) ps_file=pocketshare-darwin-arm64 ;; Darwin-x86_64) ps_file=pocketshare-darwin-x64 ;; *) ps_die "no pocketshare build for $(uname -s) $(uname -m)" ;; esac if command -v sha256sum >/dev/null 2>&1; then ps_sha() { sha256sum "$1" | cut -d ' ' -f 1; }; else ps_sha() { shasum -a 256 "$1" | cut -d ' ' -f 1; }; fi if command -v curl >/dev/null 2>&1; then ps_fetch() { curl -fsSL --retry 3 -o "$2" "$1"; }; else ps_fetch() { wget -q -O "$2" "$1"; }; fi ps_tmp=$(mktemp -d) trap 'rm -rf "$ps_tmp"' EXIT printf '%s\n' "$ps_key" >"$ps_tmp/release-key.pem" ps_fetch "$ps_downloads/$ps_version/release-manifest.json" "$ps_tmp/release-manifest.json" || ps_die "could not download $ps_downloads/$ps_version/release-manifest.json" ps_fetch "$ps_downloads/$ps_version/release-manifest.json.sig" "$ps_tmp/release-manifest.json.sig" || ps_die "could not download the manifest signature" "$ps_openssl" pkeyutl -verify -pubin -inkey "$ps_tmp/release-key.pem" -rawin \ -in "$ps_tmp/release-manifest.json" -sigfile "$ps_tmp/release-manifest.json.sig" >/dev/null 2>&1 || ps_die "the release manifest is not signed with PocketShare's release key; nothing was installed" ps_field() { sed -n "s/^ \"$1\": \"\\(.*\\)\",\$/\\1/p" "$ps_tmp/release-manifest.json"; } [ "$(ps_field product)" = pocketshare ] || ps_die "the release manifest is not for the pocketshare CLI" ps_release=$(ps_field version) case "$ps_release" in '' | *[!0-9.]*) ps_die "unexpected version '$ps_release' in the manifest" ;; esac [ "$ps_version" = latest ] || [ "$ps_version" = "$ps_release" ] || ps_die "asked for $ps_version, the manifest is $ps_release" ps_want=$(awk -v name="\"$ps_file\"," '$1 == "\"name\":" { this = ($2 == name) } this && $1 == "\"sha256\":" { gsub(/[",]/, "", $2); print $2; exit }' "$ps_tmp/release-manifest.json") ps_fetch "$ps_downloads/$ps_release/$ps_file" "$ps_tmp/pocketshare" || ps_die "could not download $ps_file" if [ -z "$ps_want" ] || [ "$(ps_sha "$ps_tmp/pocketshare")" != "$ps_want" ]; then ps_die "$ps_file does not match the signed release manifest; nothing was installed" fi # Checked from the install directory: /tmp may be mounted noexec. mkdir -p "$ps_dir" mv "$ps_tmp/pocketshare" "$ps_dir/.pocketshare.new" chmod 755 "$ps_dir/.pocketshare.new" if ! "$ps_dir/.pocketshare.new" help 2>/dev/null | grep -q '^ run '; then rm -f "$ps_dir/.pocketshare.new" ps_die "pocketshare $ps_release has no \`run\` command (or does not run here); use a newer release" fi mv "$ps_dir/.pocketshare.new" "$ps_dir/pocketshare" printf 'Installed pocketshare %s (%s, signature and SHA-256 verified) in %s\n' "$ps_release" "$ps_file" "$ps_dir" POCKETSHARE_INSTALLER sh "${TMPDIR:-/tmp}/install-pocketshare-cli.sh" - export PATH="$POCKETSHARE_INSTALL_DIR:$PATH" - pocketshare version after_script: # A backstop: if the job was killed before `pocketshare run` finished, stop its rental. - | if [ -s "$POCKETSHARE_ARTIFACTS/rental-id" ] && ! grep -q '"stopped": true' "$POCKETSHARE_ARTIFACTS/pocketshare-run.json" 2>/dev/null; then "$POCKETSHARE_INSTALL_DIR/pocketshare" stop "$(cat "$POCKETSHARE_ARTIFACTS/rental-id")" || true fi artifacts: when: always expire_in: 14 days paths: - $POCKETSHARE_ARTIFACTS/The GitHub action
Section titled “The GitHub action”ci/github/pocketshare-run/action.yml in the PocketShare repository, with the same inlined
installer. It is meant to be copied: there is no public repository to uses: it from.
# PocketShare run: a GitHub Actions composite action that rents a real Android phone on# PocketShare, connects it to adb (ANDROID_SERIAL), runs your command, saves a screenshot,# Logcat and optionally a screen recording, and always releases the phone.## PocketShare's repository isn't public, so there is no `uses: pocketshare/...` to point at:# copy this file into your repository as .github/actions/pocketshare-run/action.yml and use# it as `uses: ./.github/actions/pocketshare-run`. Documentation: https://pocketshare.io/docs/ci/## - uses: ./.github/actions/pocketshare-run# with:# api-key: ${{ secrets.POCKETSHARE_API_KEY }}# model: Pixel 9a# max-spend: '1.00'# command: ./gradlew connectedDebugAndroidTestname: PocketShare rundescription: Rent a real Android phone on PocketShare, run a command against it with adb, and always release it.inputs: api-key: description: A PocketShare API key with phones:read, rentals:read and rentals:write (a secret). required: true command: description: The command, run with bash once the phone is in `adb devices`; ANDROID_SERIAL names it. required: true max-spend: description: The rental's spending cap in dollars; billing stops it there. required: true phone: description: A phone ID. Leave empty to pick by model, brand, Android version and price. default: '' model: description: Rent this model, e.g. "Pixel 9a". default: '' brand: description: Rent a phone of this brand, e.g. google. default: '' android: description: Lowest Android version, e.g. 14. default: '' max-price: description: Highest hourly price in dollars. default: '' wait: description: How long to wait for a matching phone to become available. default: 10m timeout: description: The command's time limit; it is stopped after it (exit code 124). default: 30m record: description: Record the screen (true or false). default: 'false' artifacts: description: Directory for the screenshot, Logcat, recording and pocketshare-run.json. default: pocketshare-artifacts upload-artifacts: description: Upload the artifacts directory with actions/upload-artifact (true or false). default: 'true' cli-version: description: The pocketshare CLI version (x.y.z), or latest. default: latestoutputs: exit-code: description: The exit code of `pocketshare run` (the command's own once it ran). value: ${{ steps.run.outputs.exit-code }} rental-id: description: The rental's ID. value: ${{ steps.run.outputs.rental-id }}runs: using: composite steps: - name: Install the PocketShare CLI (signature and SHA-256 verified) shell: bash env: POCKETSHARE_CLI_VERSION: ${{ inputs.cli-version }} POCKETSHARE_INSTALL_DIR: ${{ runner.temp }}/pocketshare/bin run: | cat >"$RUNNER_TEMP/install-pocketshare-cli.sh" <<'POCKETSHARE_INSTALLER' #!/bin/sh # Installs the pocketshare CLI for a CI job, after checking it against PocketShare's signed # release manifest: the manifest's Ed25519 signature with the pinned release key (OpenSSL 3), # then the executable's SHA-256 from that manifest. Nothing is installed if either fails. # # POCKETSHARE_CLI_VERSION x.y.z, or latest (default) # POCKETSHARE_INSTALL_DIR where to put `pocketshare` (default: $HOME/.local/bin) # POCKETSHARE_DOWNLOADS the downloads (default: https://pocketshare.io/downloads/cli) # OPENSSL an OpenSSL 3 binary (default: openssl; macOS: Homebrew openssl@3) # # Linux (glibc) on x64 or arm64 and macOS. The same script is inlined in # ci/pocketshare.gitlab-ci.yml and ci/github/pocketshare-run/action.yml (a test keeps the # copies identical). Documentation: https://pocketshare.io/docs/ci/ set -eu ps_downloads=${POCKETSHARE_DOWNLOADS:-https://pocketshare.io/downloads/cli} ps_version=${POCKETSHARE_CLI_VERSION:-latest} ps_dir=${POCKETSHARE_INSTALL_DIR:-$HOME/.local/bin} ps_openssl=${OPENSSL:-openssl} # PocketShare's release key, pocketshare-release-2026 (releases/keys/ in the repository). ps_key='-----BEGIN PUBLIC KEY----- MCowBQYDK2VwAyEAoLkDjloA3xXQmioSxTrBuKFygyrt867Jrm3PNSyyLSE= -----END PUBLIC KEY-----' ps_die() { printf 'pocketshare install: %s\n' "$*" >&2 exit 1 } case "$ps_version" in latest | [0-9]*.[0-9]*.[0-9]*) ;; *) ps_die "bad POCKETSHARE_CLI_VERSION $ps_version" ;; esac case "$("$ps_openssl" version 2>/dev/null)" in 'OpenSSL '[3-9]*) ;; *) ps_die "needs OpenSSL 3 to check the release signature (set OPENSSL on macOS: brew install openssl@3)" ;; esac case "$(uname -s)-$(uname -m)" in Linux-x86_64 | Linux-amd64) ps_file=pocketshare-linux-x64 ;; Linux-aarch64 | Linux-arm64) ps_file=pocketshare-linux-arm64 ;; Darwin-arm64) ps_file=pocketshare-darwin-arm64 ;; Darwin-x86_64) ps_file=pocketshare-darwin-x64 ;; *) ps_die "no pocketshare build for $(uname -s) $(uname -m)" ;; esac if command -v sha256sum >/dev/null 2>&1; then ps_sha() { sha256sum "$1" | cut -d ' ' -f 1; }; else ps_sha() { shasum -a 256 "$1" | cut -d ' ' -f 1; }; fi if command -v curl >/dev/null 2>&1; then ps_fetch() { curl -fsSL --retry 3 -o "$2" "$1"; }; else ps_fetch() { wget -q -O "$2" "$1"; }; fi ps_tmp=$(mktemp -d) trap 'rm -rf "$ps_tmp"' EXIT printf '%s\n' "$ps_key" >"$ps_tmp/release-key.pem" ps_fetch "$ps_downloads/$ps_version/release-manifest.json" "$ps_tmp/release-manifest.json" || ps_die "could not download $ps_downloads/$ps_version/release-manifest.json" ps_fetch "$ps_downloads/$ps_version/release-manifest.json.sig" "$ps_tmp/release-manifest.json.sig" || ps_die "could not download the manifest signature" "$ps_openssl" pkeyutl -verify -pubin -inkey "$ps_tmp/release-key.pem" -rawin \ -in "$ps_tmp/release-manifest.json" -sigfile "$ps_tmp/release-manifest.json.sig" >/dev/null 2>&1 || ps_die "the release manifest is not signed with PocketShare's release key; nothing was installed" ps_field() { sed -n "s/^ \"$1\": \"\\(.*\\)\",\$/\\1/p" "$ps_tmp/release-manifest.json"; } [ "$(ps_field product)" = pocketshare ] || ps_die "the release manifest is not for the pocketshare CLI" ps_release=$(ps_field version) case "$ps_release" in '' | *[!0-9.]*) ps_die "unexpected version '$ps_release' in the manifest" ;; esac [ "$ps_version" = latest ] || [ "$ps_version" = "$ps_release" ] || ps_die "asked for $ps_version, the manifest is $ps_release" ps_want=$(awk -v name="\"$ps_file\"," '$1 == "\"name\":" { this = ($2 == name) } this && $1 == "\"sha256\":" { gsub(/[",]/, "", $2); print $2; exit }' "$ps_tmp/release-manifest.json") ps_fetch "$ps_downloads/$ps_release/$ps_file" "$ps_tmp/pocketshare" || ps_die "could not download $ps_file" if [ -z "$ps_want" ] || [ "$(ps_sha "$ps_tmp/pocketshare")" != "$ps_want" ]; then ps_die "$ps_file does not match the signed release manifest; nothing was installed" fi # Checked from the install directory: /tmp may be mounted noexec. mkdir -p "$ps_dir" mv "$ps_tmp/pocketshare" "$ps_dir/.pocketshare.new" chmod 755 "$ps_dir/.pocketshare.new" if ! "$ps_dir/.pocketshare.new" help 2>/dev/null | grep -q '^ run '; then rm -f "$ps_dir/.pocketshare.new" ps_die "pocketshare $ps_release has no \`run\` command (or does not run here); use a newer release" fi mv "$ps_dir/.pocketshare.new" "$ps_dir/pocketshare" printf 'Installed pocketshare %s (%s, signature and SHA-256 verified) in %s\n' "$ps_release" "$ps_file" "$ps_dir" POCKETSHARE_INSTALLER sh "$RUNNER_TEMP/install-pocketshare-cli.sh" echo "$POCKETSHARE_INSTALL_DIR" >>"$GITHUB_PATH" - name: Run the command on a rented phone id: run shell: bash # Inputs reach the script as environment variables, never pasted into it. env: POCKETSHARE_API_KEY: ${{ inputs.api-key }} PS_COMMAND: ${{ inputs.command }} PS_MAX_SPEND: ${{ inputs.max-spend }} PS_PHONE: ${{ inputs.phone }} PS_MODEL: ${{ inputs.model }} PS_BRAND: ${{ inputs.brand }} PS_ANDROID: ${{ inputs.android }} PS_MAX_PRICE: ${{ inputs.max-price }} PS_WAIT: ${{ inputs.wait }} PS_TIMEOUT: ${{ inputs.timeout }} PS_RECORD: ${{ inputs.record }} PS_ARTIFACTS: ${{ inputs.artifacts }} run: | args=(run --max-spend "$PS_MAX_SPEND" --artifacts "$PS_ARTIFACTS") [ -z "$PS_PHONE" ] || args+=("$PS_PHONE") [ -z "$PS_MODEL" ] || args+=(--model "$PS_MODEL") [ -z "$PS_BRAND" ] || args+=(--brand "$PS_BRAND") [ -z "$PS_ANDROID" ] || args+=(--android "$PS_ANDROID") [ -z "$PS_MAX_PRICE" ] || args+=(--max-price "$PS_MAX_PRICE") [ -z "$PS_WAIT" ] || args+=(--wait "$PS_WAIT") [ -z "$PS_TIMEOUT" ] || args+=(--timeout "$PS_TIMEOUT") [ "$PS_RECORD" != true ] || args+=(--record) set +e pocketshare "${args[@]}" -- bash -c "$PS_COMMAND" code=$? set -e echo "exit-code=$code" >>"$GITHUB_OUTPUT" if [ -s "$PS_ARTIFACTS/rental-id" ]; then echo "rental-id=$(cat "$PS_ARTIFACTS/rental-id")" >>"$GITHUB_OUTPUT"; fi exit "$code" - name: Stop the rental if the run was cut short if: always() shell: bash env: POCKETSHARE_API_KEY: ${{ inputs.api-key }} PS_ARTIFACTS: ${{ inputs.artifacts }} run: | if [ -s "$PS_ARTIFACTS/rental-id" ] && ! grep -q '"stopped": true' "$PS_ARTIFACTS/pocketshare-run.json" 2>/dev/null; then pocketshare stop "$(cat "$PS_ARTIFACTS/rental-id")" || true fi - name: Upload the PocketShare artifacts if: always() && inputs.upload-artifacts == 'true' uses: actions/upload-artifact@v4 with: name: pocketshare-${{ github.job }}-${{ github.run_attempt }} path: ${{ inputs.artifacts }} if-no-files-found: ignore