Pear Docs

Preflight a release before staging

Check a deployment directory and your write access before you stage, then confirm every platform reached the link: pear build --json, pear info's writable field, a stage dry run, and the Targets list. Includes a CI version.

Run these checks between pear build and pear stage, and once more after the stage. They catch the release problems that otherwise show up late: an architecture missing from the deployment directory, a machine that cannot write to the link you are about to stage to, and a platform that never reached the link.

Steps 1 and 2 need Pear 3.6.0 or later. Before 3.6.0, pear build printed nothing when it succeeded, and pear info had no writable field. Step 4 needs Pear 3.7.0 or later, which is the first release where pear info lists the platforms a link carries. Run pear versions to see which you have.

Need the pear CLI? Install it from install.pears.com, or prefix any command below with npx. See Install & upgrade for details.

Before you begin

You need the per-OS makes collected on one machine, a stage link from pear touch, and jq for the scripted checks. If you have not built a deployment directory before, follow Deploy your application through step 3 first. Step 1 below does what its step 4 does, and adds a report.

1. Build, and list what landed

Run pear build with --json so every artifact it places is reported as its own event, and keep the output:

pear build --json \
  --package=./pear-chat/package.json \
  --darwin-arm64-app ./pear-chat/out/PearChat-darwin-arm64/PearChat.app \
  --darwin-x64-app   ./pear-chat/out/PearChat-darwin-x64/PearChat.app \
  --linux-arm64-app  ./pear-chat/out/PearChat-linux-arm64/PearChat.AppImage \
  --linux-x64-app    ./pear-chat/out/PearChat-linux-x64/PearChat.AppImage \
  --win32-x64-app    ./pear-chat/out/PearChat-win32-x64/PearChat.msix \
  --target pear-chat-1.0.1 > build.ndjson

Without --json, the same run prints the app name, version, and target, then one + <file> (origin: <source>) line for each artifact, and ends with Build complete!. That is enough to read by eye. The --json form is the one to script against. It emits one object per line, tagged building (once, with name, version, and target), executable (once per artifact, with file and origin), and final. The pear build reference describes all three:

{"cmd":"build","tag":"building","data":{"name":"PearChat","version":"1.0.1","target":"pear-chat-1.0.1"}}
{"cmd":"build","tag":"executable","data":{"file":"/work/pear-chat-1.0.1/by-arch/linux-arm64/app/PearChat.AppImage","origin":"/work/pear-chat/out/PearChat-linux-arm64/PearChat.AppImage"}}
{"cmd":"build","tag":"executable","data":{"file":"/work/pear-chat-1.0.1/by-arch/darwin-arm64/app/PearChat.app","origin":"/work/pear-chat/out/PearChat-darwin-arm64/PearChat.app"}}
{"cmd":"build","tag":"executable","data":{"file":"/work/pear-chat-1.0.1/by-arch/linux-x64/app/PearChat.AppImage","origin":"/work/pear-chat/out/PearChat-linux-x64/PearChat.AppImage"}}
{"cmd":"build","tag":"executable","data":{"file":"/work/pear-chat-1.0.1/by-arch/darwin-x64/app/PearChat.app","origin":"/work/pear-chat/out/PearChat-darwin-x64/PearChat.app"}}
{"cmd":"build","tag":"executable","data":{"file":"/work/pear-chat-1.0.1/by-arch/win32-x64/app/PearChat.msix","origin":"/work/pear-chat/out/PearChat-win32-x64/PearChat.msix"}}
{"cmd":"build","tag":"final","data":{"success":true}}

An executable event is reported as each copy finishes, so the lines can come in a different order from your flags, as they do above. A failed build writes one error object to the file instead of a final, and exits non-zero, so nothing shows on the terminal. Check the exit status, or open the file, before you trust the counts below.

An executable event carries no architecture field. The architecture is the folder name after by-arch/ in file, so count events per architecture by reading it from the path:

jq -r 'select(.tag == "executable") | .data.file
       | capture(".*[/\\\\]by-arch[/\\\\](?<arch>[^/\\\\]+)[/\\\\]app[/\\\\]").arch' build.ndjson | sort | uniq -c

Each architecture you passed should appear. The count per architecture is the number of artifacts you passed for it, which is usually 1. If you pass a flag more than once to ship a standalone binary beside the desktop app, expect that many.

2. Check that this machine can write

pear info reports whether this machine holds the key pair for a drive. Run it against the stage link before you stage:

pear info --json pear://qxenz5wmspmryjc13m9yzsqj1conqotn8fb4ocbufwtz9mtbqq5o \
  | jq -e 'select(.tag == "info") | .data
           | if has("writable") then .writable == true
             else error("this Pear does not report writable") end'

jq -e exits non-zero when the answer is false, when writable is missing (a Pear older than 3.6.0), or when no info object came back at all, so the command works as a gate in a script. In a script, run set -o pipefail first so a failing pear info fails the gate too, and keep stdout clean, because a stray non-JSON line makes jq fail. In the human-readable output the same answer is the Writable: row (the writable row before Pear 3.7.0).

Run it against a link you have staged to at least once. A link you have only just created with pear touch has no data on this machine yet, so pear info prints [ Empty ] and sends an empty object instead of info. The gate then fails even though you hold the key. On a first release, skip this step and let the first real stage be the test.

writable: false means this machine's corestore does not have the key pair for that link. It is the first thing to check when a stage fails because this machine cannot write to the drive, since staged and provisioned drives are machine-bound. See Recovering from lost write access.

Two limits to keep in mind:

  • It describes the stage (or provision) link. A multisig production link is written by a quorum of signers, so no machine holds a key pair for it, and writable reads false there even when signing works. Do not use it to gate a production release. See Sign with multisig.
  • It tells you whether this machine holds the key, not whether the stage will succeed. It does not check that the link is seeded or reachable.

3. Dry-run the stage

With the deployment directory complete and write access confirmed, dry-run the stage and read the file-by-file diff before running the real one. A dry run does not write, so it cannot detect missing write access. That is what step 2 is for.

pear stage --dry-run pear://qxenz5wmspmryjc13m9yzsqj1conqotn8fb4ocbufwtz9mtbqq5o ./pear-chat-1.0.1

Step 1 checks what pear build placed in the deployment directory. This step checks what the link now carries. pear info lists one entry per by-arch/ folder in the drive under Targets:, with its size:

pear info pear://qxenz5wmspmryjc13m9yzsqj1conqotn8fb4ocbufwtz9mtbqq5o

To gate on it, read the targets list from --json and compare it with the architectures you build. This fails and names the ones that are missing:

pear info --json pear://qxenz5wmspmryjc13m9yzsqj1conqotn8fb4ocbufwtz9mtbqq5o \
  | jq -e --arg expected "darwin-arm64 darwin-x64 linux-arm64 linux-x64 win32-x64" 'select(.tag == "info") | .data
           | if has("targets") then
               ($expected | split(" ")) - [.targets[].platform]
               | if length == 0 then true else error("missing: " + join(" ")) end
             else error("this Pear does not report targets") end'

jq -e exits non-zero when a platform is missing, when targets is absent (a Pear older than 3.7.0), or when no info object came back. Select on "tag": "info": every line of the --json stream is its own JSON document, so a filter without it also runs on the retrieving and keys lines. A link with no data on this machine yet prints [ Empty ] and sends an empty object instead, so run the check on a machine that has staged to the link or can reach a seeder.

The sizes come from the drive's metadata. pear info does not download the blobs to report them, so you can run the check on a machine that never built the release, such as a CI job or a teammate's laptop.

Run the same checks in CI

Fail the job before it stages anything. This step reuses build.ndjson from step 1. It fails when the build did not finish cleanly or when any architecture you expect is missing:

- name: Fail on a missing architecture
  shell: bash
  env:
    EXPECTED: darwin-arm64 darwin-x64 linux-arm64 linux-x64 win32-x64
  run: |
    : "${EXPECTED:?set EXPECTED to the architectures you build}"
    [ -s build.ndjson ] || { echo "build.ndjson is missing or empty (needs Pear 3.6.0 and pear build --json)"; exit 1; }
    jq -e -s 'any(.[]; .tag == "final" and .data.success == true) and all(.[]; .tag != "error")' \
      build.ndjson > /dev/null || { echo "pear build did not finish cleanly"; exit 1; }
    found=$(jq -r 'select(.tag == "executable") | .data.file
                   | capture(".*[/\\\\]by-arch[/\\\\](?<arch>[^/\\\\]+)[/\\\\]app[/\\\\]").arch' build.ndjson | sort -u)
    status=0
    for arch in $EXPECTED; do
      grep -qxF "$arch" <<< "$found" || { echo "missing: $arch"; status=1; }
    done
    exit $status

shell: bash makes the step stop on a failed command, and it also works on a Windows runner through Git Bash.

After the stage, run the check from step 4 against the link you staged to. It reads the link from the network, so run it where pear info can reach a seeder. For a link that pear-ci staged, see Check which platforms reached the link.

Add the writable check from step 2 as a second step only when the job runs pear stage itself on a machine that holds the stage link's key pair, such as a persistent self-hosted runner. A fresh hosted runner has no key pair, and the pear-ci staging path does not use Pear's local corestore, so the check does not apply there. To produce the signed per-OS builds this assumes, see Build and sign desktop apps with GitHub Actions.

Where to go next

Last updated on

Was this helpful?

On this page