# Preflight a release before staging



Run these checks between `pear build` and `pear stage`. They catch the two release problems that otherwise show up late: an architecture missing from the [deployment directory](/pear/explanation/deployment-releasing-apps-p2p#deployment-directory-and-release-lines), and a machine that cannot write to the link you are about to stage to.

Both checks 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. Run `pear versions` to see which you have.

<Callout type="info">
  **Need the `pear` CLI?** Install it from **[install.pears.com](https://install.pears.com)**, or prefix any command below with `npx`. See [Install & upgrade](/pear/reference/pear/cli#install) for details.
</Callout>

## Before you begin [#before-you-begin]

You need the per-OS makes collected on one machine, a stage link from `pear touch`, and [`jq`](https://jqlang.org) for the scripted checks. If you have not built a deployment directory before, follow [Deploy your application](/pear/how-to/operate-an-app/manual-deployment/deployment) through step 3 first. Step 1 below does what its step 4 does, and adds a report.

## 1. Build, and list what landed [#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:

```sh skip="release-flow"
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](/pear/reference/pear/cli#pear-build) describes all three:

```json skip="sample-output"
{"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:

```sh skip="release-flow"
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 [#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:

```sh skip="release-flow"
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.

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](/pear/how-to/operate-an-app/manual-deployment/troubleshoot-desktop-releases#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](/pear/how-to/operate-an-app/multisig/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 [#3-dry-run-the-stage]

With the deployment directory complete and write access confirmed, [dry-run the stage](/pear/how-to/operate-an-app/manual-deployment/deployment#5-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.

```sh skip="release-flow"
pear stage --dry-run pear://qxenz5wmspmryjc13m9yzsqj1conqotn8fb4ocbufwtz9mtbqq5o ./pear-chat-1.0.1
```

## Run the same checks in CI [#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:

```yaml skip="ci-config"
- 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.

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](/pear/how-to/operate-an-app/github-actions/publish-with-github-actions) 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](/pear/how-to/operate-an-app/github-actions/build-and-sign-in-ci).

## Where to go next [#where-to-go-next]

* [Deploy your application](/pear/how-to/operate-an-app/manual-deployment/deployment)—the full flow these checks sit inside.
* [`pear build`](/pear/reference/pear/cli#pear-build) and [`pear info`](/pear/reference/pear/cli#pear-info)—every flag and the full `--json` shape.
* [Troubleshoot desktop releases](/pear/how-to/operate-an-app/manual-deployment/troubleshoot-desktop-releases)—staging, seeding, and lost-key problems.
