# Publish with GitHub Actions



The simplest way to publish and update a Pear app is to let CI do it. The [`pear-ci` GitHub Action](https://github.com/holepunchto/actions/blob/main/pear-ci/action.yml) stages a directory from your repository into a [Hyperdrive](/p2p/reference/building-blocks/hyperdrive) on every push, so shipping a new version is just a `git push`. There is no local `pear touch`, `pear seed`, or `pear stage` to run by hand, and nothing to keep online between releases.

This is the goal-oriented recipe. If you need the full-control flow—release lines, provision, and multisig—use [Deploy your application](/pear/how-to/operate-an-app/manual-deployment/deployment) instead. The two are not mutually exclusive: CI is the fast path for development and staging links, while the manual flow gives you production gating.

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

## How it works [#how-it-works]

The action wraps the [`pear-ci`](https://www.npmjs.com/package/pear-ci) tool. On each run it:

1. Installs `pear-ci` and fetches the committed `ci/snapshot.json` from `main`.
2. Mirrors your `target` directory into a [Hyperdrive](/p2p/reference/building-blocks/hyperdrive) whose key is **derived deterministically from `primary-key` + `namespace`**, then replicates over [Hyperswarm](/p2p/reference/building-blocks/hyperswarm) until connected peers have synced.
3. Commits the updated `ci/snapshot.json` back to `main` (or opens a PR if the push is rejected).

Two properties make this work well in CI:

* **The `pear://` link is stable.** Because the drive key comes from the secret primary key plus the namespace, every run stages to the same link. You set that link once as your app's `upgrade` field and every push updates it.
* **The snapshot replaces persistent storage.** `ci/snapshot.json` records each core's length, so the next run only syncs what changed instead of re-uploading the whole drive or persisting a full [Corestore](/p2p/reference/helpers/corestore) between jobs.

<Mermaid
  chart="flowchart LR
  push[&#x22;git push to main&#x22;] --> action[&#x22;pear-ci action&#x22;]
  action --> snap[&#x22;fetch ci/snapshot.json&#x22;]
  snap --> stage[&#x22;stage target into Hyperdrive&#x22;]
  stage --> sync[&#x22;replicate over Hyperswarm&#x22;]
  sync --> commit[&#x22;commit updated snapshot.json&#x22;]
  stage --> link[&#x22;stable pear:// link&#x22;]"
/>

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

You need:

1. A GitHub repository for your app.
2. The directory you want to publish committed to that repo—typically the [`pear build`](/pear/how-to/operate-an-app/build-and-package/build-desktop-distributables#build-the-deployment-directory) deployment directory, your app source if you stage the runnable project directly, or a mobile OTA payload's `dist/` (see [Build the OTA payload](/pear/how-to/operate-an-app/manual-deployment/mobile#build-the-ota-payload-steps-34)).
3. A 32-byte hex **primary key** (created below) stored as a repository secret.
4. A **namespace** name (any string of letters, numbers, dots, hyphens, or underscores) that identifies this drive.

## Generate and store the primary key [#generate-and-store-the-primary-key]

Generate a random 32-byte key:

```bash skip="manual-walkthrough"
openssl rand -hex 32
# 9f2c... (64 hex characters)
```

Add it to your repository as a secret named `PEAR_PRIMARY_KEY` (**Settings → Secrets and variables → Actions → New repository secret**).

<Callout type="warning">
  The primary key is the **write key** for your drive—anyone who has it can publish updates that your users will receive over the air. Keep it in a repository (or organization) secret, never commit it, and rotate it if it leaks. Rotating the key changes the derived `pear://` link, so you would need to update the `upgrade` field and re-onboard users.
</Callout>

## Add the workflow [#add-the-workflow]

Create `.github/workflows/pear-ci.yml`:

```yaml skip="ci-config"
name: Publish with pear-ci

on:
  push:
    branches: [main]

# pear-ci commits the updated snapshot back to main
permissions:
  contents: write
  pull-requests: write

concurrency:
  group: pear-ci
  cancel-in-progress: false

jobs:
  stage:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: holepunchto/actions/pear-ci@main
        with:
          primary-key: ${{ secrets.PEAR_PRIMARY_KEY }}
          namespace: my-app
          target: dist
          dry-run: false
```

Set `namespace` to your chosen name and `target` to the repo-relative path of the directory you want to publish. The `concurrency` group prevents two runs from racing to commit `ci/snapshot.json` at the same time.

For the full list of inputs and outputs, see the [`pear-ci` GitHub Action reference](/pear/reference/ci-and-release/pear-ci-action).

## Wire up the `pear://` link [#wire-up-the-pear-link]

The first run prints the drive key it stages to:

```text skip="sample-output"
starting stage for key <drive-key>
```

That key is your app's `pear://` link. Set it as the [`upgrade`](/pear/reference/pear/configuration) field so installed builds poll it for updates:

```bash skip="manual-walkthrough"
npm pkg set upgrade=pear://<drive-key>
```

This is the same `upgrade` link that the [manual deploy flow](/pear/how-to/operate-an-app/manual-deployment/deployment#1-set-the-upgrade-link) sets—the only difference is that CI now stages to it for you. Because the key is deterministic, you only do this once.

## Dry-run first [#dry-run-first]

Before publishing for real, set `dry-run: true` in the workflow. The action computes and logs the file-by-file diff without writing anything to the drive—the same safety check as `pear stage --dry-run`:

```yaml skip="ci-config"
        with:
          primary-key: ${{ secrets.PEAR_PRIMARY_KEY }}
          namespace: my-app
          target: dist
          dry-run: true
```

Read the diff in the Actions log. Look for the files you expect, no surprise additions (`.DS_Store`, editor swap files, secrets), and sensible byte counts. When it looks right, set `dry-run: false` and push. Every subsequent push to `main` then stages and publishes automatically.

## Producing the build to stage [#producing-the-build-to-stage]

`pear-ci` stages whatever is in `target`—the action itself has no idea whether that directory came from a desktop or a mobile build. To build and sign a desktop directory on CI first, pair it with [`make-pear-app`](/pear/reference/ci-and-release/github-actions#make-pear-app): one job produces signed, notarized distributables (see [Build and sign desktop apps in CI](/pear/how-to/operate-an-app/github-actions/build-and-sign-in-ci)), and `pear-ci` stages the assembled deployment directory to your stable `pear://` link.

<Callout type="info">
  For a mobile OTA payload, point `target` at `dist`—the directory [`npm run update`](/pear/how-to/operate-an-app/manual-deployment/mobile#build-the-ota-payload-steps-34) assembles—instead of a desktop deployment directory. There is no equivalent of `make-pear-app` for mobile signing yet, so the build step itself (bundling and `pear build`) still has to run somewhere before `pear-ci` sees `dist`; if you add that as its own CI job, remember the worker bundle is gitignored and needs [an explicit rebuild step](/pear/how-to/operate-an-app/build-and-package/submit-to-app-stores#ios) in CI too, not just before a local `eas build`.
</Callout>

## See also [#see-also]

* [`pear-ci` GitHub Action reference](/pear/reference/ci-and-release/pear-ci-action)—every input, output, and the underlying CLI.
* [Holepunch GitHub Actions](/pear/reference/ci-and-release/github-actions)—the other shared actions, including `make-pear-app`.
* [Build and sign desktop apps in CI](/pear/how-to/operate-an-app/github-actions/build-and-sign-in-ci)—build and sign the artifacts `pear-ci` stages.
* [Deploy your application](/pear/how-to/operate-an-app/manual-deployment/deployment)—the full manual flow with release lines, provision, and multisig.
* [Ship your app](/p2p/getting-started/build-a-peer-to-peer-chat/ship)—the type-along walkthrough of staging by hand.
* [Configuration](/pear/reference/pear/configuration)—the `upgrade` field and other `package.json` settings.
* [Release pipeline](/pear/explanation/deployment-releasing-apps-p2p)—the conceptual picture of staging and OTA updates.
* [Troubleshoot desktop releases](/pear/how-to/operate-an-app/manual-deployment/troubleshoot-desktop-releases)—when an update does not reach peers.
