pear-ci GitHub Action
Reference for the holepunchto/actions/pear-ci GitHub Action—inputs, outputs, validation, and the underlying pear-ci CLI for staging a Pear app from CI.
The pear-ci GitHub Action stages a directory from your repository into a Hyperdrive on every run, producing a stable pear:// link your app can use as its upgrade field. For the task-oriented setup, see Publish with GitHub Actions.
It is a composite action that installs the pear-ci npm tool, fetches the committed snapshot from main, stages, and commits the updated snapshot back. Every run except a dry run of an empty drive needs a peer that holds the drive, as Peers and timeouts describes.
Each run installs the latest pear-ci from npm, whichever version of the action you pin: @v1 pins the action's steps, not the tool. Where this page names pear-ci 0.1.2, it describes that release.
Usage
- uses: holepunchto/actions/pear-ci@v1
with:
primary-key: ${{ secrets.PEAR_PRIMARY_KEY }}
namespace: my-app
target: dist
dry-run: falseInputs
| Input | Required | Default | Description |
|---|---|---|---|
primary-key | Yes | — | 32-byte (64 hex character) key for the Corestore identity. Determines the drive's pear:// link across runs. Store it as a secret. |
namespace | Yes | — | Drive namespace name. Combined with primary-key to derive the drive. Only letters, numbers, dots, hyphens, and underscores. |
target | Yes | — | Repo-relative path of the directory to stage. Must not be absolute or contain ... |
dry-run | No | false | When true, computes and logs the diff without writing to the drive or committing the snapshot. Must be true or false. |
github-token | No | ${{ github.token }} | Token used to fetch and commit ci/snapshot.json. |
app-slug | No | — | When set, the snapshot commit is attributed to the matching GitHub App bot identity instead of github-actions[bot]. |
Outputs
| Output | Description |
|---|---|
length | The drive's length after staging, the block count of its metadata core, which pear info reports as length. Not set when dry-run is true. |
pr-url | URL of the PR created if a direct push of the snapshot to main failed; empty otherwise. |
Input validation
The action rejects malformed inputs before staging:
primary-keymust match^[a-fA-F0-9]{64}$.namespacemust match^[a-zA-Z0-9._-]+$.targetmust contain only[a-zA-Z0-9_./-], must not be an absolute path, and must not contain..(no directory traversal).dry-runmust be exactlytrueorfalse.
Snapshot commits
The action reads ci/snapshot.json from a branch named main before staging and writes it back to main afterward. The action hard-codes the branch name. The snapshot records each core's length so subsequent runs sync only what changed. When dry-run is true, no commit is made.
The file must exist before the first run. Commit it to main containing an empty list, []. Without it, the Fetch snapshot from main step fails with gh: Not Found (HTTP 404).
Grant the workflow contents: write (and pull-requests: write if you want the PR fallback) so the snapshot commit can land.
The action commits the snapshot on top of the commit the run checked out. If pushing it directly to main fails, the action pushes a pear-ci/snapshot-<run-id> branch and opens a PR, exposing its URL via the pr-url output. The push fails when someone pushed to main during the run, and on every run if branch protection blocks direct pushes to main. Until you merge that PR, main holds an older snapshot than the drive, so every run fails before it stages with Core <key> length (<n>) is higher than length in snapshot.json. Merge it before the next run.
With the default github-token, the PR fallback also needs Allow GitHub Actions to create and approve pull requests turned on in the repository's Actions settings. Without it, the step fails after it pushes the branch, and you have to open the PR yourself.
Commit [] only once per drive, before its first release. After that, the action keeps ci/snapshot.json up to date. Never reset it to [], and never commit it empty or as invalid JSON: pear-ci treats a file it can't parse as [], so a stray comma or a leftover merge-conflict marker does the same damage as a reset. Publish each drive from one repository. If you move publishing to another repository, copy the latest ci/snapshot.json across instead of starting from [].
A run that starts from [] against a drive that already has releases writes a second history under the same key. pear-ci 0.1.2 logs [hypercore] conflict detected, still exits with code 0, and the seeder's view of the drive drops to length 0. pear-ci 0.1.2 can't recover the drive from this state. To publish again, set a new namespace, commit [] for the new drive, and seed the new link that a dry run prints. A new namespace means a new pear:// link, so you have to update the upgrade field and re-onboard users, as with a rotated primary key.
Peers and timeouts
A runner starts with no copy of the drive, so each run relies on a peer that holds it, such as an always-on machine running pear seed on the drive's link. pear-ci waits on that peer twice, with no time limit:
- Before staging, until a peer reports each core at its length in the snapshot. It logs
syncing <key> to length <n>for each core, dry runs included. If a peer reports a core longer than the snapshot records, the run fails withCore <key> length (<n>) is higher than length in snapshot.json. That happens while a snapshot PR from an earlier run isn't merged yet (see Snapshot commits), and after a run stopped partway through its upload (see the warning below). - After staging, until a peer holds every block of the drive, the new release included. It logs
waiting synced remote peers for key <drive-key>, thenall remote peers synced.
With no peer online, a run waits at one of these lines until a peer connects or the job's timeout-minutes ends it, which defaults to 360 minutes. Starting a seeder while the run waits lets it finish. Set timeout-minutes on the job, well over the time your largest release takes to reach the seeder.
Don't stop a run after it prints starting stage for key while a peer is connected. From that line on, the run writes the release, and a connected peer can learn the release's new length before it has every block. If a timeout or a cancel stops the run at that point, the peer keeps the new length but misses blocks that only the stopped runner had. pear-ci 0.1.2 can't recover from this state: later runs fail with Core <key> length (<n>) is higher than length in snapshot.json, and a snapshot edited to the new length leaves the run waiting at waiting synced remote peers until it times out.
The job log doesn't show peer connections, so check the seeder instead. pear seed prints a peer join line for each peer that connects, the runner included, and while it's still downloading, its Drive Length and Blobs Length rows show synced and total blocks, such as 1532/1646.
Underlying CLI
Outside of Actions, the same staging runs through the pear-ci CLI:
npm install -g pear-ci
pear-ci \
--primary-key <64-hex-key> \
--name my-app \
--target ./dist \
--snapshot ci/snapshot.json \
--storage /tmp/store| Flag | Description |
|---|---|
--primary-key | 32-byte hex key for the Corestore. |
--name | Namespace name used to derive the drive. |
--target | Local directory to mirror into the drive. |
--snapshot | Path to read/write the JSON snapshot. |
--storage | Corestore storage path. |
--dry-run | Compute the diff without writing to the drive. |
--output | Path to write the staged drive's key (z32) and length to, as JSON: { "key": "<drive-key>", "length": <n> }. Added in pear-ci 0.1.2. |
The CLI logs the drive key it stages to (starting stage for key <drive-key>)—that key is your pear:// link. With --output, it also writes the key and length to a file, so a script does not have to parse the log. The action uses it to set its length output. See the pear-ci README for the programmatic API and snapshot format.
See also
- Publish with GitHub Actions—step-by-step setup.
- Holepunch GitHub Actions—the full catalog of shared actions this is part of.
- Configuration—the
upgradefield this link feeds. - Hyperdrive and Hyperswarm—the building blocks
pear-cistages and replicates over. - Deploy your application—the manual staging flow.
Last updated on