diff --git a/CLAUDE.md b/CLAUDE.md index 050e020..cb31692 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -9,6 +9,7 @@ Everything in this repository is — or will be — public. Treat every file, co **Brand capitalization: always `Quicknode` (capital Q, lowercase n).** Never `QuickNode`, `quickNode`, or `QUICKNODE`. This applies to every surface — code, comments, commit messages, docs, generated workflow strings, formula descriptions, package metadata, error messages, anywhere the brand name appears. - **Never commit secrets.** No API keys, tokens, account IDs, customer identifiers, internal hostnames, or non-public URLs. The CLI itself reads keys from env vars or `~/.config/qn/config.toml`; both live outside the repo. If you spot a leaked secret in a diff, stop and flag it before committing. +- **Don't mention internal secret names either, even ones that don't exist yet.** Names invented for proposed/planned secrets (e.g. for a release pipeline that needs a PAT we haven't created) are still internal infrastructure detail and read as roadmap leaks. Use generic language in code comments, commit messages, PR descriptions, etc. ("a CI secret for tap push", "a PAT with contents:write on the bucket") rather than a specific env var name. *Standard*, externally-documented names that come from a public tool's own docs are fine to mention (e.g. `CARGO_REGISTRY_TOKEN` is the name `cargo publish` itself reads). - **Never include internal Quicknode information** in code, comments, commit messages, fixtures, snapshots, or test data: no internal Slack/Linear/Jira links, no employee names, no internal team structure, no unreleased product/feature names, no non-public roadmap detail, no private infrastructure details. If something would be inappropriate to post on the public crates.io page, it doesn't belong here. - **Test fixtures must use fake data.** Endpoint IDs like `ep-1`, wallets like `0xabc`, URLs like `https://hook.example.com`, emails like `alice@example.com`. Don't paste real responses from a real account. - **No internal-only justifications in commit messages or comments.** "Fixes the bug from the Slack thread" leaks the existence of the Slack thread. State the technical reason instead: "Fixes incorrect URL path for `update_endpoint_status`." diff --git a/Justfile b/Justfile index 76a13c8..dfa3260 100644 --- a/Justfile +++ b/Justfile @@ -42,10 +42,10 @@ release-cargo-publish: cargo publish -p quicknode-cli # Manually update the Homebrew tap with the formula attached to a given -# release. Use this until we have a HOMEBREW_TAP_TOKEN secret and can -# add "homebrew" to publish-jobs in dist-workspace.toml — at which -# point the cargo-dist workflow takes over and this recipe becomes -# obsolete. +# release. Use this until CI has a PAT with contents:write on the tap +# repo and can automate the formula push — at which point we add +# "homebrew" to publish-jobs in dist-workspace.toml, the cargo-dist +# workflow takes over, and this recipe becomes a manual-recovery fallback. # # Usage: just release-update-homebrew-tap 0.1.0 ~/qn/homebrew-tap # @@ -84,6 +84,113 @@ release-update-homebrew-tap version tap_path: echo "Committed qn {{version}} to {{tap_path}}. To publish:" echo " git -C {{tap_path}} push" +# Manually update the AUR `qn-bin` package with a PKGBUILD + .SRCINFO +# for a given release. Use this until CI has SSH access to push to the +# AUR git remote — at which point a CI publish-aur job takes over and +# this recipe becomes a manual-recovery fallback. +# +# The PKGBUILD downloads our prebuilt linux-gnu tarballs from the GitHub +# release (separate sha256 per arch). x86_64 and aarch64 only — cargo-dist +# does not build i686 or armv7. +# +# Usage: just release-update-aur-bin 0.1.4 ~/qn/aur-qn-bin +# +# Precondition: aur_path is a clean local clone of +# ssh://aur@aur.archlinux.org/qn-bin.git. First-time setup requires the +# package name `qn-bin` to be registered on the AUR — see AUR docs for +# the initial `git clone` + push. +release-update-aur-bin version aur_path: + #!/usr/bin/env bash + set -euo pipefail + if [[ ! -d "{{aur_path}}/.git" ]]; then + echo "Error: {{aur_path}} is not a git checkout. Clone ssh://aur@aur.archlinux.org/qn-bin.git there first." >&2 + exit 1 + fi + if ! git -C "{{aur_path}}" diff --quiet || ! git -C "{{aur_path}}" diff --cached --quiet; then + echo "Error: {{aur_path}} has uncommitted changes. Commit or stash them first." >&2 + exit 1 + fi + # Fetch both sha256 sidecars from the release. + x86_url="https://github.com/quicknode/cli/releases/download/v{{version}}/quicknode-cli-x86_64-unknown-linux-gnu.tar.xz.sha256" + arm_url="https://github.com/quicknode/cli/releases/download/v{{version}}/quicknode-cli-aarch64-unknown-linux-gnu.tar.xz.sha256" + if ! x86_line=$(curl -sfL "$x86_url"); then + echo "Error: could not download $x86_url (does the release exist?)" >&2 + exit 1 + fi + if ! arm_line=$(curl -sfL "$arm_url"); then + echo "Error: could not download $arm_url" >&2 + exit 1 + fi + # Each sidecar is ` *` — take just the hex. + x86_hash=$(echo "$x86_line" | awk '{print $1}') + arm_hash=$(echo "$arm_line" | awk '{print $1}') + for h in "$x86_hash" "$arm_hash"; do + if [[ ! "$h" =~ ^[0-9a-f]{64}$ ]]; then + echo "Error: parsed hash '$h' is not a 64-char hex string." >&2 + exit 1 + fi + done + cat > "{{aur_path}}/PKGBUILD" < + pkgname=qn-bin + pkgver={{version}} + pkgrel=1 + pkgdesc='Command-line interface for the Quicknode SDK' + arch=('x86_64' 'aarch64') + url='https://github.com/quicknode/cli' + license=('MIT') + depends=('glibc') + provides=('qn') + conflicts=('qn') + source_x86_64=("\$pkgname-\$pkgver-x86_64.tar.xz::https://github.com/quicknode/cli/releases/download/v\$pkgver/quicknode-cli-x86_64-unknown-linux-gnu.tar.xz") + source_aarch64=("\$pkgname-\$pkgver-aarch64.tar.xz::https://github.com/quicknode/cli/releases/download/v\$pkgver/quicknode-cli-aarch64-unknown-linux-gnu.tar.xz") + sha256sums_x86_64=('$x86_hash') + sha256sums_aarch64=('$arm_hash') + + package() { + local archdir + case "\$CARCH" in + x86_64) archdir='quicknode-cli-x86_64-unknown-linux-gnu' ;; + aarch64) archdir='quicknode-cli-aarch64-unknown-linux-gnu' ;; + esac + install -Dm755 "\$archdir/qn" "\$pkgdir/usr/bin/qn" + install -Dm644 "\$archdir/LICENSE" "\$pkgdir/usr/share/licenses/\$pkgname/LICENSE" + install -Dm644 "\$archdir/README.md" "\$pkgdir/usr/share/doc/\$pkgname/README.md" + } + EOF + # Generate .SRCINFO manually — we can't run `makepkg --printsrcinfo` on + # macOS dev boxes. Mirrors the deterministic field order makepkg emits. + cat > "{{aur_path}}/.SRCINFO" < 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 +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). + +## 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 --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. diff --git a/dist-workspace.toml b/dist-workspace.toml index bcb41ca..6a96c47 100644 --- a/dist-workspace.toml +++ b/dist-workspace.toml @@ -18,10 +18,11 @@ hosting = "github" # Whether to install an updater program install-updater = false # Homebrew tap repository to publish the formula to. The "homebrew" -# entry is deliberately omitted from publish-jobs until we have a -# HOMEBREW_TAP_TOKEN secret — without it, the formula push step would -# fail CI on every release. The formula itself is still generated and -# attached to each GitHub Release. Until the token exists, run +# entry is deliberately omitted from publish-jobs until CI has a PAT +# with contents:write on the tap repo — without that secret, the +# formula push step would fail CI on every release. The formula +# itself is still generated and attached to each GitHub Release. +# Until that's wired, run # `just release-update-homebrew-tap VERSION TAP_PATH` to push the # formula to quicknode/homebrew-tap manually. tap = "quicknode/homebrew-tap"