quicknode-cli 0.6.0

Command-line interface for the Quicknode SDK
Documentation
# Releasing `qn`

How to cut a release. The pipeline is mostly automated via cargo-dist; a few channels still need manual maintainer steps until CI has the right credentials to do them itself.

## Quick release

If the one-time setup below is done (tap, bucket, and AUR clones live under `~/qn/`), a full release is two commands:

```fish
just release-prepare X.Y.Z
# release-prepare drives bump → PR → squash-merge → tag → CI through to a green run.
# Watch the workflow; come back when it's done.

just release-finalize
# Auto-detects the just-released version from the latest git tag, then bumps
# the Homebrew tap, Scoop bucket, and AUR qn-bin clones to match. Opens a PR
# against the tap and bucket repos (their main branches are protected) and
# prints the `git push` command for AUR. Merge the two PRs and run the push
# to publish.
```

Override the clone-root directory if your clones live elsewhere: `just release-finalize ~/work/quicknode`. Override the version too if you're backfilling an older release: `just release-finalize ~/qn 0.1.4`.

The rest of this document covers what each step does in detail, what to do if part of the pipeline fails, and the one-time setup for each channel.

## Per-release flow

Three named recipes in the `Justfile`:

1. **`just release-prepare X.Y.Z`** — orchestrates the bump → branch → PR → squash-merge → tag-push → wait-for-CI sequence. Tag push is what fires `release.yml`; do not also run `release-create-tag` (it races cargo-dist's host job).

   Internally calls `release-bump`, `release-open-pr`, `release-merge-pr`, `release-tag-main`, `release-wait-ci`. Each is also runnable standalone if a step fails and you need to retry.

2. **`release.yml` runs in CI** — cross-compiles 7 targets, creates the GitHub Release, attaches archives + sha256 sidecars + SLSA attestations, then fans out to per-channel publish jobs.

   Publish channels currently in CI:
   - `custom-publish-crates` → publishes `quicknode-cli` to crates.io
   - `custom-publish-docker` → builds multi-arch image, pushes to `ghcr.io/quicknode/qn`
   - `custom-publish-deb` → packages `.deb` per arch, uploads to the GitHub Release as assets
   - `custom-publish-copr` → builds an SRPM from `packaging/qn-bin.spec` (whose `%prep` downloads the SLSA-attested prebuilt binary), dispatches via `copr-cli build` to the `quicknode/qn` COPR project

3. **Maintainer manual steps after CI succeeds.** Some channels still need a person to drive the publish because CI doesn't yet have the credentials it needs.

## Manual steps after each release

Run these from the repo root (`~/qn/cli`) after `release.yml` is green for the new tag.

### Homebrew

Sync the formula cargo-dist generated as a release artifact into the tap repo:

```fish
just release-update-homebrew-tap X.Y.Z ~/qn/homebrew-tap
```

The recipe downloads `qn.rb` from the GitHub Release, copies it to `Formula/qn.rb` on a `release/vX.Y.Z` branch cut from the tap's latest `origin/main`, commits, pushes the branch, and opens a PR (the tap's `main` is protected, so changes must land via PR). Review and merge the PR to publish. Re-running the recipe recreates the branch and updates the open PR.

### Scoop

Bump the canonical `version` in `bucket/qn.json`:

```fish
just release-update-scoop-bucket X.Y.Z ~/qn/scoop-bucket
```

The recipe pulls the Windows zip's sha256 from the release, renders a manifest with `version`, `hash`, and an `autoupdate` block at `bucket/qn.json`, commits it on a `release/vX.Y.Z` branch cut from the bucket's latest `origin/main`, pushes the branch, and opens a PR (the bucket's `main` is protected, so changes must land via PR). Review and merge the PR to publish. Once a user has tapped the bucket, `scoop update` finds new versions on its own — this manual step just keeps `scoop search qn` honest about what's current.

### AUR

Bump `pkgver` in the `qn-bin` AUR package:

```fish
just release-update-aur-bin X.Y.Z ~/qn/qn-bin
git -C ~/qn/qn-bin push
```

The recipe pulls both Linux gnu sha256 sidecars (x86_64 + aarch64) from the release, renders a `PKGBUILD` + `.SRCINFO`, and stages them. Push goes to `ssh://aur@aur.archlinux.org/qn-bin.git` — the AUR's git remote.

### Curated install block on the release notes

`release-finalize` finishes by calling `release-update-install-notes`, which prepends a curated "How to install" section to the GitHub release body. Source for the block is `packaging/release-notes-install.md.tmpl`; edit it there if the install copy needs to change. The recipe is idempotent — re-running against the same release replaces the existing block rather than stacking duplicates — so it's safe to invoke standalone:

```fish
just release-update-install-notes X.Y.Z            # edits the release in place
just release-update-install-notes X.Y.Z --dry-run  # prints the assembled body without editing
```

cargo-dist's auto-generated content is preserved below a `---` separator.

## One-time setup notes

A few channels needed manual setup the first time. Captured here so the next maintainer doesn't have to rediscover them.

### Homebrew tap (`quicknode/homebrew-tap`)

Public repo on GitHub. Must be public — `brew tap` does an anonymous git clone. Has a single `Formula/qn.rb` per formula. cargo-dist generates the formula as a release artifact (whether or not we auto-publish), so the maintainer's job is just to commit it into the tap.

### Scoop bucket (`quicknode/scoop-bucket`)

Public repo on GitHub. Must be public — Scoop does an anonymous git clone. Has `bucket/qn.json` per package. We hand-render the manifest in the recipe (cargo-dist doesn't generate one).

### AUR (`qn-bin`)

Maintainer needs an AUR account at <https://aur.archlinux.org> with an SSH key registered. Once that's set up:

```fish
# Clone the (currently empty) AUR git remote
mkdir -p ~/qn
cd ~/qn
git clone ssh://aur@aur.archlinux.org/qn-bin.git
# AUR returns: "warning: You appear to have cloned an empty repository." — expected.

# Render PKGBUILD + .SRCINFO from the latest release
cd ~/qn/cli
just release-update-aur-bin X.Y.Z ~/qn/qn-bin

# AUR expects the default branch to be `master`. Modern git defaults to `main`,
# so rename the freshly-created branch before the first push.
git -C ~/qn/qn-bin branch -m main master
git -C ~/qn/qn-bin push -u origin master
```

The first push registers the package on the AUR. Subsequent pushes just update it — no rename needed (the branch stays `master`).

After publishing: confirm via `https://aur.archlinux.org/packages/qn-bin` (the RPC at `/rpc/v5/info` can lag the package page by a few minutes — trust the web page, not the RPC, for fresh registrations).

### COPR (`quicknode/qn`)

Maintainer needs a Fedora account at <https://accounts.fedoraproject.org> and a COPR project `quicknode/qn` created at <https://copr.fedorainfracloud.org>. Project settings to set on creation:

- **Chroots**: current Fedora releases + EPEL 9 (covers RHEL 9, Rocky 9, Alma 9). Skip EPEL 8 unless requested — its glibc is too old for our gnu binaries.
- **Build settings**: enable internet access for builds (the `%prep` step in `packaging/qn-bin.spec` curls the prebuilt tarball from the GitHub Release; without net the build can't download it).

CI auths to COPR via four repo secrets — `copr-cli` itself only reads credentials from a config file at `~/.config/copr` (no env-var fallback), so we provision the four fields separately and let the workflow assemble the file at build time. Generate the values at <https://copr.fedorainfracloud.org/api/>; the page shows a `[copr-cli]` config block with these four lines. Copy each field's value into its own secret:

| Secret | Field from the COPR API page |
|---|---|
| `COPR_LOGIN`    | `login = …` |
| `COPR_USERNAME` | `username = …` |
| `COPR_TOKEN`    | `token = …` |
| `COPR_URL`      | `copr_url = …` (almost always `https://copr.fedorainfracloud.org`) |

Set each with `gh secret set COPR_LOGIN --repo quicknode/cli` etc., pasting just the value (no `login = ` prefix, no quotes). Splitting them this way also means the token can be rotated without re-pasting the other three.

`packaging/qn-bin.spec` lives in this repo; it's a thin spec whose `%prep` downloads the SLSA-attested prebuilt linux-gnu tarball and verifies it against the `.sha256` sidecar, so COPR isn't rebuilding `qn` from Rust source — it's just packaging the upstream binary into an RPM per chroot. Same trust chain as everywhere else qn ships.

## Recovery: a publish channel failed

If a single publish-* job in `release.yml` fails (e.g. crates.io rejected the publish because the token expired), the rest of the release is still good — the GitHub Release, attestations, and other channels remain published.

To retry just the failed job:

```fish
gh run rerun <run-id> --failed --repo quicknode/cli
```

For crates.io specifically, the manual fallback if CI's auth is broken is:

```fish
just release-cargo-publish
# Requires `cargo login` first.
```

## Sanity-checks before tagging

- `just lint` clean (`cargo clippy --all-targets -- -D warnings`)
- `just test` clean
- `just release-cargo-publish-check` clean (validates the crate tarball without uploading)
- `dist plan` exits 0 (verifies the generated workflow matches `dist-workspace.toml`)

If `dist plan` complains the workflow is out of date, run `just dist-regen` to regenerate and commit the result.