From 8869c91a4975822d674c802c45e0caef15263249 Mon Sep 17 00:00:00 2001 From: John Mitsch Date: Thu, 11 Jun 2026 10:44:44 -0400 Subject: [PATCH 1/3] Document Scoop and AUR install paths in README Scoop bucket has been seeded for v0.1.4 and the AUR qn-bin package is registered + installable via yay. Two new install snippets so users on Windows + Arch don't have to know about the GitHub Releases page. Homebrew was already in the install matrix. --- README.md | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/README.md b/README.md index f0dbba4..5e60eda 100644 --- a/README.md +++ b/README.md @@ -43,6 +43,19 @@ The crate name is `quicknode-cli` but the installed binary is `qn`. brew install quicknode/tap/qn ``` +### Scoop (Windows) + +```powershell +scoop bucket add quicknode https://github.com/quicknode/scoop-bucket +scoop install quicknode/qn +``` + +### Arch Linux (AUR) + +```sh +yay -S qn-bin # or any other AUR helper +``` + ### From source ```sh From ef100f4e9720f7f565ecca109f2e40e1898acfc4 Mon Sep 17 00:00:00 2001 From: John Mitsch Date: Thu, 11 Jun 2026 10:45:10 -0400 Subject: [PATCH 2/3] Wire COPR publish into the release pipeline (Phase 7) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds COPR (Fedora + EPEL) to the cargo-dist user_publish_jobs pipeline. Same shape as the existing crates.io / Docker / .deb publish jobs. Why the spec-file approach: copr-cli's only build entrypoint accepts SRPMs or spec files — it doesn't import prebuilt binary RPMs. So the binary-everywhere property we have for crates.io, GHCR, .deb, and AUR needed a thin spec whose %prep downloads the SLSA-attested upstream tarball cargo-dist already published, verifies it against the .sha256 sidecar, and then %install just lays the binary into the buildroot. COPR's mock chroot does this on each Fedora + EPEL chroot in ~30 seconds — no Rust toolchain involved, single trust chain. Why four secrets instead of one combined config blob: copr-cli only reads credentials from ~/.config/copr (no env-var fallback per copr/v3/helpers.py's config_from_file). The workflow has to assemble that file at runtime regardless. Splitting the four fields (login, username, token, copr_url) into separate repo secrets makes each one safe to paste as a single line (no multiline blob to get malformed) and lets the token be rotated without touching the other three. Changes: * packaging/qn-bin.spec — the thin spec. ExclusiveArch: x86_64 aarch64. Per-arch Source URL via ifarch, sha256 verification in %prep before %setup. * .github/workflows/publish-copr.yml — reusable workflow invoked by cargo-dist. Builds the SRPM from the spec (with QN_VERSION passed in from the dist manifest), assembles ~/.config/copr from the four COPR_* secrets, then dispatches via `copr-cli build --enable-net=on quicknode/qn `. --enable-net=on is required because %prep curls the tarball; COPR's default mock has network disabled. * dist-workspace.toml — adds ./publish-copr to publish-jobs and grants `contents: read` via github-custom-job-permissions. * RELEASING.md — documents the COPR channel under CI publish channels and the four-secret provisioning flow. * release.yml — regenerated to include custom-publish-copr. The job is gated by cargo-dist on `!is_prerelease`, same as the others. Until all four COPR_* secrets are set on the repo, the job will fail loudly at the "Configure copr-cli" step with a clear error listing which fields are missing — the rest of the release continues to succeed. Earlier in this branch I went down a wrong path: a `release-build-copr-rpms` Justfile recipe + [package.metadata.generate-rpm] block that produced binary RPMs locally with cargo-generate-rpm. `copr-cli build` doesn't accept binary RPMs, so that path was a dead-end. Reset and rewrote before opening the PR for review. --- .github/workflows/publish-copr.yml | 112 +++++++++++++++++++++++++++++ .github/workflows/release.yml | 16 ++++- RELEASING.md | 23 +++++- dist-workspace.toml | 4 +- packaging/qn-bin.spec | 79 ++++++++++++++++++++ 5 files changed, 230 insertions(+), 4 deletions(-) create mode 100644 .github/workflows/publish-copr.yml create mode 100644 packaging/qn-bin.spec diff --git a/.github/workflows/publish-copr.yml b/.github/workflows/publish-copr.yml new file mode 100644 index 0000000..117a1cf --- /dev/null +++ b/.github/workflows/publish-copr.yml @@ -0,0 +1,112 @@ +name: Publish RPMs to COPR + +# Reusable workflow invoked by cargo-dist's release pipeline as a +# user_publish_job (see dist-workspace.toml `publish-jobs`). +# +# Builds an SRPM from packaging/qn-bin.spec — a thin .spec whose %prep +# downloads the SLSA-attested prebuilt binary cargo-dist publishes for +# this release and verifies it against the .sha256 sidecar. No Rust +# toolchain involved on COPR's side; the binary inside the resulting +# RPM is bit-identical to the one in crates.io, Homebrew, GHCR, .deb, +# and AUR. +# +# `copr-cli build --enable-net=on` is required because the spec's +# %prep stage fetches the tarball over HTTPS from the GitHub Release. +on: + workflow_call: + inputs: + plan: + description: dist-manifest JSON for this announcement + required: true + type: string + +# Reusable-workflow permissions must not exceed what the caller grants +# in dist-workspace.toml's github-custom-job-permissions block. We only +# need to read this repo to access the spec file. +permissions: + contents: read + +jobs: + publish: + runs-on: ubuntu-24.04 + steps: + - uses: actions/checkout@v4 + + - name: Extract release tag from plan + id: meta + env: + PLAN: ${{ inputs.plan }} + run: | + version=$(echo "$PLAN" | jq -r '.releases[0].app_version') + echo "version=$version" >> "$GITHUB_OUTPUT" + + - name: Install rpmbuild + copr-cli + run: | + sudo apt-get update + sudo apt-get install -y rpm python3-pip python3-venv + python3 -m venv "$HOME/copr-venv" + "$HOME/copr-venv/bin/pip" install --quiet copr-cli rich + echo "$HOME/copr-venv/bin" >> "$GITHUB_PATH" + + - name: Build SRPM + env: + QN_VERSION: ${{ steps.meta.outputs.version }} + run: | + # Set up the rpmbuild tree. + mkdir -p "$HOME/rpmbuild"/{SOURCES,SPECS,SRPMS} + cp packaging/qn-bin.spec "$HOME/rpmbuild/SPECS/qn-bin.spec" + + # SRPM only has the spec — the sources are fetched by mock at + # %prep time (Source0/Source1 are URLs, not local files). We + # pass --nodeps so rpmbuild doesn't try to satisfy + # BuildRequires on the runner; COPR's mock handles that. + rpmbuild -bs "$HOME/rpmbuild/SPECS/qn-bin.spec" \ + --define "_topdir $HOME/rpmbuild" \ + --define "qn_version $QN_VERSION" \ + --nodeps + ls -la "$HOME/rpmbuild/SRPMS/" + + - name: Configure copr-cli + env: + # copr-cli doesn't read env vars — it requires a config file at + # ~/.config/copr in INI format. The four fields are provisioned + # as separate repo secrets so each can be pasted as a single + # line and rotated independently. The file is assembled here at + # runtime. Verbatim values come from + # https://copr.fedorainfracloud.org/api/. + COPR_LOGIN: ${{ secrets.COPR_LOGIN }} + COPR_USERNAME: ${{ secrets.COPR_USERNAME }} + COPR_TOKEN: ${{ secrets.COPR_TOKEN }} + COPR_URL: ${{ secrets.COPR_URL }} + run: | + missing=() + [[ -z "$COPR_LOGIN" ]] && missing+=(COPR_LOGIN) + [[ -z "$COPR_USERNAME" ]] && missing+=(COPR_USERNAME) + [[ -z "$COPR_TOKEN" ]] && missing+=(COPR_TOKEN) + [[ -z "$COPR_URL" ]] && missing+=(COPR_URL) + if (( ${#missing[@]} > 0 )); then + echo "Error: COPR secrets not set on this repo:" >&2 + printf ' %s\n' "${missing[@]}" >&2 + echo "Provision per RELEASING.md's COPR one-time-setup section." >&2 + exit 1 + fi + mkdir -p ~/.config + # Use printf rather than heredoc to avoid YAML-indentation + # leaking into the file (configparser treats indented lines + # as continuations and rejects them). + printf '[copr-cli]\nlogin = %s\nusername = %s\ntoken = %s\ncopr_url = %s\n' \ + "$COPR_LOGIN" "$COPR_USERNAME" "$COPR_TOKEN" "$COPR_URL" \ + > ~/.config/copr + chmod 600 ~/.config/copr + + - name: copr-cli build + run: | + srpm=$(ls "$HOME/rpmbuild/SRPMS/"qn-${{ steps.meta.outputs.version }}-1*.src.rpm) + if [[ ! -f "$srpm" ]]; then + echo "Error: SRPM not found at expected path under SRPMS/" >&2 + exit 1 + fi + echo "Uploading $srpm to quicknode/qn..." + # --enable-net=on so COPR's mock chroot can curl the prebuilt + # tarball + sha256 sidecar from the GitHub Release in %prep. + copr-cli build quicknode/qn "$srpm" --enable-net=on diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index dc49a00..b306fac 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -327,6 +327,19 @@ jobs: permissions: "contents": "write" + custom-publish-copr: + needs: + - plan + - host + if: ${{ !fromJson(needs.plan.outputs.val).announcement_is_prerelease || fromJson(needs.plan.outputs.val).publish_prereleases }} + uses: ./.github/workflows/publish-copr.yml + with: + plan: ${{ needs.plan.outputs.val }} + secrets: inherit + # publish jobs get escalated permissions + permissions: + "contents": "read" + announce: needs: - plan @@ -334,10 +347,11 @@ jobs: - custom-publish-crates - custom-publish-docker - custom-publish-deb + - custom-publish-copr # use "always() && ..." to allow us to wait for all publish jobs while # still allowing individual publish jobs to skip themselves (for prereleases). # "host" however must run to completion, no skipping allowed! - if: ${{ always() && needs.host.result == 'success' && (needs.custom-publish-crates.result == 'skipped' || needs.custom-publish-crates.result == 'success') && (needs.custom-publish-docker.result == 'skipped' || needs.custom-publish-docker.result == 'success') && (needs.custom-publish-deb.result == 'skipped' || needs.custom-publish-deb.result == 'success') }} + if: ${{ always() && needs.host.result == 'success' && (needs.custom-publish-crates.result == 'skipped' || needs.custom-publish-crates.result == 'success') && (needs.custom-publish-docker.result == 'skipped' || needs.custom-publish-docker.result == 'success') && (needs.custom-publish-deb.result == 'skipped' || needs.custom-publish-deb.result == 'success') && (needs.custom-publish-copr.result == 'skipped' || needs.custom-publish-copr.result == 'success') }} runs-on: "ubuntu-22.04" env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/RELEASING.md b/RELEASING.md index 3bb4849..75fd8ad 100644 --- a/RELEASING.md +++ b/RELEASING.md @@ -16,8 +16,9 @@ Three named recipes in the `Justfile`: - `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.** Three channels still need a person to drive the publish because CI doesn't yet have the credentials it needs. +3. **Maintainer manual steps after CI succeeds.** Two channels still need a person to drive the publish because CI doesn't yet have the credentials it needs. ## Manual steps after each release @@ -98,6 +99,26 @@ The first push registers the package on the AUR. Subsequent pushes just update i 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 and a COPR project `quicknode/qn` created at . 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 ; 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. diff --git a/dist-workspace.toml b/dist-workspace.toml index 6a96c47..b8b7e31 100644 --- a/dist-workspace.toml +++ b/dist-workspace.toml @@ -29,7 +29,7 @@ tap = "quicknode/homebrew-tap" # Override the Homebrew formula name so `brew install quicknode/tap/qn` works # (default would derive it from the package name `quicknode-cli`). formula = "qn" -publish-jobs = ["./publish-crates", "./publish-docker", "./publish-deb"] +publish-jobs = ["./publish-crates", "./publish-docker", "./publish-deb", "./publish-copr"] # Emit SLSA build attestations for every binary archive. Critical for a credential-handling tool. github-attestations = true @@ -41,4 +41,4 @@ github-attestations = true # the calling job's permission grant so the called workflow can also # declare contents:read without exceeding the caller. publish-deb # needs contents: write so it can `gh release upload` the .deb files. -github-custom-job-permissions = { "publish-crates" = { contents = "read" }, "publish-docker" = { contents = "read", packages = "write", "id-token" = "write" }, "publish-deb" = { contents = "write" } } +github-custom-job-permissions = { "publish-crates" = { contents = "read" }, "publish-docker" = { contents = "read", packages = "write", "id-token" = "write" }, "publish-deb" = { contents = "write" }, "publish-copr" = { contents = "read" } } diff --git a/packaging/qn-bin.spec b/packaging/qn-bin.spec new file mode 100644 index 0000000..1b9c9ab --- /dev/null +++ b/packaging/qn-bin.spec @@ -0,0 +1,79 @@ +# qn-bin: install the SLSA-attested binary cargo-dist ships, no rebuild. +# +# This spec is built on COPR's mock chroots. `%prep` downloads the per-arch +# linux-gnu tarball from the GitHub Release and verifies it against the +# .sha256 sidecar; `%install` lays the binary + docs into the buildroot. +# No Rust toolchain involved on the COPR side — the binary inside the +# resulting RPM is bit-identical to what ships in crates.io, +# Homebrew, .deb, and the GHCR image. +# +# Built and uploaded by .github/workflows/publish-copr.yml on each release. +# Requires --enable-net=on at build time (mock fetches the tarball). + +%global qn_version %{getenv:QN_VERSION} + +%if "%{qn_version}" == "" +%{error: QN_VERSION must be set when building this spec (e.g. rpmbuild --define "qn_version 0.1.4")} +%endif + +Name: qn +Version: %{qn_version} +Release: 1%{?dist} +Summary: Command-line interface for the Quicknode SDK +License: MIT +URL: https://github.com/quicknode/cli + +# cargo-dist emits separate tarballs per Rust target triple. We map COPR's +# arch tokens to those triples; the per-arch Source entry below picks the +# right one at build time. +%ifarch x86_64 +%global rust_target x86_64-unknown-linux-gnu +%endif +%ifarch aarch64 +%global rust_target aarch64-unknown-linux-gnu +%endif + +Source0: https://github.com/quicknode/cli/releases/download/v%{version}/quicknode-cli-%{rust_target}.tar.xz +Source1: https://github.com/quicknode/cli/releases/download/v%{version}/quicknode-cli-%{rust_target}.tar.xz.sha256 + +ExclusiveArch: x86_64 aarch64 +BuildRequires: coreutils +BuildRequires: tar +BuildRequires: xz + +%description +qn is a command-line interface for Quicknode, built around noun-verb +commands that read naturally for both humans and agents. Manage endpoints, +streams, webhooks, the KV store, teams, usage, and billing. + +This package installs the prebuilt binary cargo-dist publishes upstream — +the same SLSA-attested artifact that ships in crates.io, Homebrew, the +GHCR Docker image, the AUR qn-bin package, and Debian .deb files. + +%prep +# Verify the tarball matches the sha256 sidecar from the release. +# The sidecar's format is ` *`; rewrite the filename to +# point at the local SOURCES path so `sha256sum -c` works. +expected_hash=$(awk '{print $1}' < %{SOURCE1}) +actual_hash=$(sha256sum %{SOURCE0} | awk '{print $1}') +if [ "$expected_hash" != "$actual_hash" ]; then + echo "Error: sha256 mismatch for %{SOURCE0}" >&2 + echo " expected: $expected_hash" >&2 + echo " actual: $actual_hash" >&2 + exit 1 +fi +%setup -q -n quicknode-cli-%{rust_target} + +%install +install -Dm755 qn %{buildroot}%{_bindir}/qn +install -Dm644 LICENSE %{buildroot}%{_licensedir}/%{name}/LICENSE +install -Dm644 README.md %{buildroot}%{_docdir}/%{name}/README.md + +%files +%{_bindir}/qn +%license LICENSE +%doc README.md + +%changelog +* Thu Jun 11 2026 Quicknode - %{version}-1 +- Automated build from the GitHub Release upstream. From 598847ca5e11d99606abca8aa4a9862dbb170e0f Mon Sep 17 00:00:00 2001 From: John Mitsch Date: Thu, 11 Jun 2026 11:43:53 -0400 Subject: [PATCH 3/3] Add release-sync-manual-channels + bring Scoop recipe into this PR MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `release-prepare` ships the automated half of a release end-to-end: bump, PR, merge, tag, CI runs all the in-CI publish jobs. The other half (Homebrew, Scoop, AUR — currently maintainer-driven because we don't yet have the credentials to automate the pushes) was three separate `just release-update-*` invocations with three local clone paths to remember and three push commands to type. Easy to forget, easy to skip a channel. This commit collapses that to one wrapper: just release-sync-manual-channels Auto-detects the just-released version from the latest git tag, expects three sibling clones under ~/qn/ (the established convention across this repo's recipes), runs the three release-update-* recipes in sequence, then prints the three `git -C ... push` commands the maintainer reviews and runs to publish. Both arguments are optional and overridable: just release-sync-manual-channels ~/work/quicknode # alt root just release-sync-manual-channels ~/qn 0.1.4 # backfill Also folds in the `release-update-scoop-bucket` recipe (previously in PR #16, which is now redundant — close it after this lands). Scrubbed the invented secret-name reference from its comment per CLAUDE.md. Adds a "Quick release" section at the top of RELEASING.md showing the happy-path two-command flow: just release-prepare X.Y.Z # … wait for CI … just release-sync-manual-channels The detailed per-channel sections stay below it as reference for when something fails or the maintainer needs to drive a single channel manually. --- Justfile | 121 +++++++++++++++++++++++++++++++++++++++++++++++++++ RELEASING.md | 26 ++++++++--- 2 files changed, 141 insertions(+), 6 deletions(-) diff --git a/Justfile b/Justfile index dfa3260..3de5d06 100644 --- a/Justfile +++ b/Justfile @@ -191,6 +191,127 @@ release-update-aur-bin version aur_path: echo "Committed qn-bin {{version}} to {{aur_path}}. To publish:" echo " git -C {{aur_path}} push" +# Manually update the Scoop bucket with a manifest for a given release. +# Use this until CI has a PAT with contents:write on the bucket repo and +# can automate the push — at which point a CI job takes over and this +# recipe becomes a manual-recovery fallback. +# +# The generated manifest includes `checkver` + `autoupdate` so Scoop +# users get new versions on `scoop update` even without us pushing a +# new manifest for every release. We still push to bump the canonical +# `version` field so `scoop search` is honest about what's current. +# +# Usage: just release-update-scoop-bucket 0.1.4 ~/qn/scoop-bucket +# +# Precondition: bucket_path is a clean local clone of quicknode/scoop-bucket. +release-update-scoop-bucket version bucket_path: + #!/usr/bin/env bash + set -euo pipefail + if [[ ! -d "{{bucket_path}}/.git" ]]; then + echo "Error: {{bucket_path}} is not a git checkout. Clone quicknode/scoop-bucket there first." >&2 + exit 1 + fi + if ! git -C "{{bucket_path}}" diff --quiet || ! git -C "{{bucket_path}}" diff --cached --quiet; then + echo "Error: {{bucket_path}} has uncommitted changes. Commit or stash them first." >&2 + exit 1 + fi + sha_url="https://github.com/quicknode/cli/releases/download/v{{version}}/quicknode-cli-x86_64-pc-windows-msvc.zip.sha256" + if ! sha_line=$(curl -sfL "$sha_url"); then + echo "Error: could not download $sha_url (does the release exist?)" >&2 + exit 1 + fi + # The .sha256 sidecar is ` *`; take just the hex. + hash=$(echo "$sha_line" | awk '{print $1}') + if [[ ! "$hash" =~ ^[0-9a-f]{64}$ ]]; then + echo "Error: parsed hash '$hash' is not a 64-char hex string." >&2 + exit 1 + fi + mkdir -p "{{bucket_path}}/bucket" + cat > "{{bucket_path}}/bucket/qn.json" </dev/null 2>&1; then + git add bucket/qn.json + elif git diff --quiet bucket/qn.json; then + echo "bucket/qn.json is already at v{{version}}. Nothing to commit." + exit 0 + else + git add bucket/qn.json + fi + git commit -m "qn {{version}}" + echo + echo "Committed qn {{version}} to {{bucket_path}}. To publish:" + echo " git -C {{bucket_path}} push" + +# Run release-update-{homebrew-tap,scoop-bucket,aur-bin} in sequence for +# the latest release tag, then print the three `git push` commands the +# maintainer needs to run to publish. Auto-detects the version from the +# most recent git tag (`v` → ``), or accepts an override. +# +# Expects three sibling clones under `root` (defaults to ~/qn): +# ~/qn/homebrew-tap → quicknode/homebrew-tap +# ~/qn/scoop-bucket → quicknode/scoop-bucket +# ~/qn/qn-bin → ssh://aur@aur.archlinux.org/qn-bin.git +# +# Usage: +# just release-sync-manual-channels # auto-detect version, default root +# just release-sync-manual-channels ~/work/quicknode # override root +# just release-sync-manual-channels ~/qn 0.1.4 # override both +release-sync-manual-channels root="~/qn" version="": + #!/usr/bin/env bash + set -euo pipefail + # Expand a leading ~/ to $HOME/ since bash doesn't expand tildes inside + # variables. Absolute paths pass through unchanged. + root="{{root}}" + case "$root" in + "~") root="$HOME" ;; + "~/"*) root="$HOME/${root#\~/}" ;; + esac + # If no version was passed, take the latest tag (strip leading v). + version="{{version}}" + if [[ -z "$version" ]]; then + git fetch --tags --quiet + tag=$(git describe --tags --abbrev=0 2>/dev/null || true) + if [[ -z "$tag" ]]; then + echo 'Error: no git tags found and no version override passed. Run: just release-sync-manual-channels ROOT VERSION' >&2 + exit 1 + fi + version="${tag#v}" + fi + echo "Syncing manual channels at v${version} from clones under ${root}/" + echo + just release-update-homebrew-tap "$version" "${root}/homebrew-tap" + echo + just release-update-scoop-bucket "$version" "${root}/scoop-bucket" + echo + just release-update-aur-bin "$version" "${root}/qn-bin" + echo + echo "All three channels updated. To publish, run:" + echo " git -C ${root}/homebrew-tap push" + echo " git -C ${root}/scoop-bucket push" + echo " git -C ${root}/qn-bin push" + # Release Phase 1: bump → branch → PR → merge → tag → GH release → wait for CI. # Each recipe is callable on its own; release-prepare orchestrates them with prompts. diff --git a/RELEASING.md b/RELEASING.md index 75fd8ad..4314ec5 100644 --- a/RELEASING.md +++ b/RELEASING.md @@ -2,6 +2,25 @@ 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-sync-manual-channels +# 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. Prints the +# three `git push` commands you run to publish. +``` + +Override the clone-root directory if your clones live elsewhere: `just release-sync-manual-channels ~/work/quicknode`. Override the version too if you're backfilling an older release: `just release-sync-manual-channels ~/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`: @@ -18,7 +37,7 @@ Three named recipes in the `Justfile`: - `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.** Two channels still need a person to drive the publish because CI doesn't yet have the credentials it needs. +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 @@ -74,11 +93,6 @@ Public repo on GitHub. Must be public — Scoop does an anonymous git clone. Has Maintainer needs an AUR account at with an SSH key registered. Once that's set up: ```fish -# Confirm the name isn't taken (one-time, before first push) -curl -sf "https://aur.archlinux.org/rpc/v5/info?arg[]=qn-bin" \ - | python3 -c "import json,sys; print(json.load(sys.stdin).get('resultcount'))" -# 0 = free - # Clone the (currently empty) AUR git remote mkdir -p ~/qn cd ~/qn