Skip to content

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.

Terminal window
pocketshare run --model "Pixel 9a" --max-spend 1.00 --wait 10m --timeout 30m \
--artifacts pocketshare-artifacts -- ./gradlew connectedDebugAndroidTest
  1. 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.
  2. Store it as a secret: a masked, protected CI/CD variable POCKETSHARE_API_KEY in 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-key does, if your tests call pocketshare).
  3. 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.
  4. 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 [<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:.

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.

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.

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 connectedDebugAndroidTest

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 connectedDebugAndroidTest

The 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.

Gradle’s connected tests run on the device named by ANDROID_SERIAL:

Terminal window
./gradlew assembleDebug assembleDebugAndroidTest
pocketshare run --model "Pixel 9a" --max-spend 1.00 --wait 10m --timeout 30m \
--artifacts pocketshare-artifacts -- ./gradlew connectedDebugAndroidTest

Or with adb and am instrument directly:

Terminal window
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.

Both templates install the CLI with the same short script (ci/install-pocketshare-cli.sh):

  1. Download release-manifest.json and its signature from https://pocketshare.io/downloads/cli/<version or latest>/.
  2. Check the signature with OpenSSL 3 against PocketShare’s release key, which the script pins (pocketshare-release-2026, key oLkDjloA3xXQmioSxTrBuKFygyrt867Jrm3PNSyyLSE), and that the manifest is for the pocketshare CLI and the version asked for.
  3. Download the executable for the runner and check its SHA-256 against the signed manifest.
  4. 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).

  • Ending is guaranteed by the CLI and backed up by the job. run ends 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 whose pocketshare-run.json doesn’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, run prints a warning (an ending_soon event 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. --wait holds the job while it waits for a phone; set the job’s own timeout to cover --wait plus --timeout.

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).

ci/pocketshare.gitlab-ci.yml
# 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/

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.

.github/actions/pocketshare-run/action.yml
# 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 connectedDebugAndroidTest
name: PocketShare run
description: 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: latest
outputs:
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