From ffbf63c71dba6a642618edc57f0aabcaf8bb3eeb Mon Sep 17 00:00:00 2001 From: "blu@blu.cx" <38222767+thebrandonlucas@users.noreply.github.com> Date: Tue, 13 Oct 2026 14:09:28 -0500 Subject: [PATCH] docs: add troubleshooting, getting started, a changelog, its CI check, and a PR template docs/guides/troubleshooting.md is keyed by exit code and by each credential_problem name, with the fix for each; a test requires an entry for every exit code and every CredentialProblem, which exhaustive matches keep complete. docs/getting-started.md walks from install to a settled mutinynet payment and a scripted receive. CHANGELOG.md starts with the changes since 0.1.0. A Changelog workflow fails a PR that changes src/cli.rs or api/commands.json without CHANGELOG.md, unless it has the no-changelog label. AGENTS.md states the rule and that the check is a heuristic. The pull request template asks for the changelog entry, the verification gate, the review checks, the CRAP result, and any check not run with its reason. The docs example checks now also read CHANGELOG.md and the tutorial, so their commands parse and their changes pass as offline dry runs. --- .github/pull_request_template.md | 7 +++ .github/workflows/changelog.yml | 33 ++++++++++ AGENTS.md | 10 +++ CHANGELOG.md | 31 ++++++++++ docs/README.md | 3 +- docs/getting-started.md | 65 ++++++++++++++++++++ docs/guides/troubleshooting.md | 102 +++++++++++++++++++++++++++++++ src/cli.rs | 39 ++++++++++++ tests/cli_integration.rs | 2 +- tests/support/examples.rs | 16 +++-- 10 files changed, 301 insertions(+), 7 deletions(-) create mode 100644 .github/pull_request_template.md create mode 100644 .github/workflows/changelog.yml create mode 100644 CHANGELOG.md create mode 100644 docs/getting-started.md create mode 100644 docs/guides/troubleshooting.md diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md new file mode 100644 index 0000000..c98162c --- /dev/null +++ b/.github/pull_request_template.md @@ -0,0 +1,7 @@ + + +- [ ] User-visible change: a guide or reference under `docs/` and `CHANGELOG.md` (**Unreleased**) are updated, or the PR has the `no-changelog` label. +- [ ] The verification gate in `docs/development.md` passes. +- [ ] The diff was reviewed against its base with the `voltage-pr-review` skill, and every new error and early-return path has a test. +- [ ] CRAP report, when `src/` or `tests/` changed: the highest score and each function at or above 20. +- [ ] Checks not run, and why: diff --git a/.github/workflows/changelog.yml b/.github/workflows/changelog.yml new file mode 100644 index 0000000..e1dbb94 --- /dev/null +++ b/.github/workflows/changelog.yml @@ -0,0 +1,33 @@ +name: Changelog + +on: + pull_request: + types: [opened, synchronize, reopened, labeled, unlabeled] + +permissions: + contents: read + +jobs: + changelog: + name: Changelog updated for CLI changes + if: ${{ !contains(github.event.pull_request.labels.*.name, 'no-changelog') }} + runs-on: ubuntu-24.04 + timeout-minutes: 5 + + steps: + - name: Check out repository + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + fetch-depth: 0 + persist-credentials: false + + - name: Require a CHANGELOG entry when the command surface changes + env: + BASE: ${{ github.event.pull_request.base.sha }} + run: | + changed="$(git diff --name-only "$BASE"...HEAD)" + if grep -qxE 'src/cli\.rs|api/commands\.json' <<<"$changed" \ + && ! grep -qx 'CHANGELOG.md' <<<"$changed"; then + echo "::error::src/cli.rs or api/commands.json changed without CHANGELOG.md. Add a line under Unreleased, or label the PR no-changelog." + exit 1 + fi diff --git a/AGENTS.md b/AGENTS.md index 622b5fb..e231d44 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -45,6 +45,16 @@ reviewed project documentation or a source of truth. Keep durable decisions and behavior in tracked docs, code, and tests; create a subfolder per work item and revise or remove it as work progresses. +## Documentation + +- A user-visible change updates its guide or reference under `docs/` and adds a line under + **Unreleased** in `CHANGELOG.md`. CI fails a pull request that changes `src/cli.rs` or + `api/commands.json` without `CHANGELOG.md`, unless it has the `no-changelog` label. +- That check is a heuristic, not a rule about user-visible changes. It also fires on a + test-only change to `src/cli.rs`, and it misses user-visible changes in other files, + such as `src/input.rs` or `docs/`. Add the changelog line yourself when the change is + user-visible, and add the `no-changelog` label when it fires on a change that is not. + ## Agent friction log The CLI must be easy for AI agents to use. When it is not, record why in diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..39f30cf --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,31 @@ +# Changelog + +User-visible changes to `voltage`, newest first. Each pull request that changes what users see adds a line under **Unreleased**; see [AGENTS.md](AGENTS.md). + +## Unreleased + +### Added + +- Pick a missing organization, environment, or wallet from a list on a terminal. +- `voltage context` shows the effective profile, scope, credential, API and auth URLs, and where each came from. +- `--dry-run` describes any API request offline; changes are only described unless `--execute` or `VOLTAGE_EXECUTE=1` asks to send them. +- `--no-input` and `--quiet`, with interrupt-safe progress and the time waited so far on payment waits and device approval. +- `voltage --version` shows the git build. +- Every command that changes something has examples in `--help`, and every command links its guide. +- Guides for wallets, webhooks, troubleshooting, and getting started, and references for global flags, environment variables, and configuration. + +### Changed + +- Human tables fit the terminal width, and all API text is scrubbed of terminal control characters. +- Each item of a string list gets its own line in human output. +- Errors name the next command: missing scope names the command that lists IDs, credential and profile errors list the saved names, HTTP 401 and 403 point to `voltage auth status` and `voltage context`, and `quote_required` points to `voltage quotes create`. +- An API rejection's error leads with the API's reason, and human output shows the rest of the API error as field/value lines instead of JSON. +- `-h` shows usage and options; `--help` adds notes, examples, and the guide link. + +### Fixed + +- Help and README examples for changes include `--execute`, so copying one sends it. + +## 0.1.0 - 2026-09-18 + +First release: a command for every Voltage API operation, browser login and API keys, profiles, payment waits with a request journal, hosted checkout, prices, and signed release artifacts. diff --git a/docs/README.md b/docs/README.md index 4c7f89d..6a62487 100644 --- a/docs/README.md +++ b/docs/README.md @@ -1,6 +1,6 @@ # Voltage CLI documentation -Start with the [README](../README.md) for install and a quick start. +Start with the [README](../README.md) for install and a quick start, or follow [getting started](getting-started.md) from install to a settled test payment. ## Do a task @@ -11,6 +11,7 @@ Start with the [README](../README.md) for install and a quick start. - [Requests](guides/requests.md): request bodies, friendly flags, filters, paging - [Webhooks](guides/webhooks.md): create with a saved secret, events, deliveries, key rotation - [Checkout](guides/checkout.md): session and stream tokens, events +- [Troubleshooting](guides/troubleshooting.md): what each exit code and error means, and the fix - [Scripting](guides/scripting.md): JSON, `VOLTAGE_EXECUTE`, running without a person ## Look it up diff --git a/docs/getting-started.md b/docs/getting-started.md new file mode 100644 index 0000000..4508df9 --- /dev/null +++ b/docs/getting-started.md @@ -0,0 +1,65 @@ +# Getting started + +This walks one path from install to a settled test payment on mutinynet, then the same steps in a script. It takes about ten minutes. You need a Voltage account with an organization and an environment. + +## 1. Install and sign in + +Install the CLI from the [install guide](guides/install.md), then sign in through your browser: + +```sh +voltage login --account me +``` + +## 2. Save your scope as a profile + +Find your organization and environment, then save both with your login as a profile: + +```sh +voltage organizations list +voltage environments list --org ORG_ID +voltage profiles create test --org ORG_ID --env ENV_ID --account me +voltage context --profile test +``` + +`voltage context` shows what every command with `--profile test` will use, and where each value came from. + +## 3. Find or create a mutinynet wallet + +```sh +voltage wallets list --profile test +``` + +Use a wallet on a BTC line of credit. A wallet on a USD line of credit needs a quote for every payment, so step 4 would fail with [`quote_required`](guides/troubleshooting.md#quote_required). + +If there is no such wallet, create one on a BTC line of credit from `credit-lines list`. Check the request with a dry run first, then send it with `--execute`: + +```sh +voltage credit-lines list --profile test +voltage wallets create --profile test --name tutorial \ + --credit-line CREDIT_LINE_ID --network mutinynet --limit 100000 --dry-run +voltage wallets create --profile test --name tutorial \ + --credit-line CREDIT_LINE_ID --network mutinynet --limit 100000 --execute +``` + +## 4. Receive 1,000 sats + +Create a Lightning invoice, show it as a QR code, and wait until it is paid. Pay it from any mutinynet wallet or faucet: + +```sh +voltage payments receive --profile test --wallet WALLET_ID \ + --currency btc --kind bolt11 --amount 1000 --unit sats --qr --wait completed --execute +``` + +The command prints the payment when it settles. If the wait times out (exit code 5), keep watching with `voltage payments get PAYMENT_ID --profile test --wait completed`. + +## 5. Script it + +With `--json`, each command writes one JSON envelope to stdout and nothing else, and the exit code says what happened: + +```sh +voltage payments receive --profile test --wallet WALLET_ID --json \ + --currency btc --kind bolt11 --amount 1000 --unit sats --wait ready --execute \ + | jq -r '.data.data.payment_request' +``` + +Next: [payments](guides/payments.md), [scripting](guides/scripting.md), [troubleshooting](guides/troubleshooting.md). diff --git a/docs/guides/troubleshooting.md b/docs/guides/troubleshooting.md new file mode 100644 index 0000000..6342c00 --- /dev/null +++ b/docs/guides/troubleshooting.md @@ -0,0 +1,102 @@ +# Troubleshooting + +Each entry is headed by the message or code you see. Errors print `error:` and usually a `hint:` with the next command; with `--json`, they are `error.message` and `error.hint`, and `voltage context` shows the profile, scope, and credential a command would use. + +## Exit codes + +### Exit code 1 + +The API rejected the request, or a payment reached a failed state. The message carries the HTTP status, and `--json` adds the API's response under `error.detail.data`. Fix the request, or read the payment with `voltage payments get PAYMENT_ID` before trying again. + +### Exit code 2 + +The command line or configuration is invalid: an unknown flag, a missing `--org`, `--env`, or `--wallet`, an unknown profile, or friendly flags combined with `--data`. The hint names what is missing and how to supply it. + +### Exit code 3 + +Authentication or authorization failed. See [HTTP 401](#http-401) and [HTTP 403](#http-403) below, and the credential entries after them. + +### Exit code 4 + +The connection failed. For a payment or treasury movement, the request may have been sent: the error reports the ID, and the CLI never resubmits it. Read it with `voltage payments get PAYMENT_ID` before you decide to retry. See [payment IDs](../concepts/safety-model.md#payment-ids-and-the-request-journal). + +### Exit code 5 + +A wait or paging deadline passed. A pending payment is not failed: keep watching it with `voltage payments get PAYMENT_ID --profile prod --wait completed`, or pass a longer `--timeout`. + +### Exit code 6 + +The change was described but not sent, because neither `--execute` nor `VOLTAGE_EXECUTE=1` asked for it. Read the description, then run the same command with `--execute`. + +### Exit code 130 + +You pressed Ctrl-C. Before a change is submitted, nothing was sent. If a payment may have been sent, the message says so and gives its ID; read it before you retry. + +## Credentials + +### `no_credential` + +`error: No credential is available`. Sign in with `voltage login`, import a key with `voltage auth import-key`, or set `VOLTAGE_API_KEY`. + +### `multiple_credentials` + +`error: Multiple credentials are saved`. The hint lists them; select one with `--account NAME`, or save a profile with `voltage profiles create`. + +### `unknown_credential` + +`error: Unknown credential NAME`. The hint lists the saved names. `voltage auth status --account NAME` shows one. + +### `empty_api_key` + +`error: VOLTAGE_API_KEY is empty`. A blank variable still hides saved credentials. Unset it, or set it to a key. + +### `bound_elsewhere` + +An imported API key belongs to one organization and environment, and the command selected another. Select a credential for that scope with `--account NAME`, or use a browser login. + +### `auth_url_mismatch` + +`error: Saved credentials cannot be refreshed at a different auth URL`. A browser login refreshes only at the auth URL it was saved with. Omit `--auth-url` to use that URL, which `voltage context` shows. To move the login to the new URL, run `voltage logout --account NAME`, then `voltage login --account NAME --auth-url URL`. + +### The saved login has expired or was revoked + +Replace the saved login with the two commands in the hint. `voltage login` refuses to overwrite a saved credential, so remove the stale one first: + +```sh +voltage logout --local --account NAME +voltage login --account NAME +``` + +`--local` skips server revocation, which a revoked or expired login cannot complete. Keep the same `NAME` so profiles that select it keep working. + +### needs a browser login + +`organizations list` and `environments list` need a browser login; an API key cannot list them. Pass `--account NAME` for a saved login, or unset `VOLTAGE_API_KEY`. + +### HTTP 401 + +The API did not accept the credential. `voltage auth status` shows the selected one and whether it has expired. For a saved login, replace it as described in [The saved login has expired or was revoked](#the-saved-login-has-expired-or-was-revoked). + +A checkout command presents its session or stream token, not the account credential. Check the token in `--token-file`, `VOLTAGE_CHECKOUT_TOKEN`, or `VOLTAGE_STREAM_TOKEN`. + +### HTTP 403 + +The credential is valid but cannot reach the selected organization or environment. `voltage context` shows which scope and credential the command used. + +If `voltage context` shows the credential's location as `environment`, the command used `VOLTAGE_API_KEY` instead of a saved login. Check that the key belongs to the selected organization, or unset `VOLTAGE_API_KEY` (or pass `--account NAME`) to use a saved login. + +### Cannot read OS credential store + +The keychain is locked or missing, as on headless Linux without an unlocked Secret Service. Unlock it, or log in again with `--credential-store file`. See [credential stores](../reference/configuration.md#credential-stores). + +## API features + +### `feature_flag_disabled` + +On-chain and BIP21 payments and treasury movements are enabled per organization. Ask Voltage to enable the feature for yours. + +### `quote_required` + +A wallet on a USD line of credit needs a quote for every payment. Create a quote with `voltage quotes create`, then pass `--quote QUOTE_ID` and the same USD amount to the payment. See [quotes and lines of credit](payments.md#quotes-and-lines-of-credit). + +Related: [exit codes](../reference/exit-codes.md), [environment variables](../reference/environment.md), [log in and use profiles](login-and-profiles.md). diff --git a/src/cli.rs b/src/cli.rs index 78d1055..8eb7f03 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -1247,6 +1247,7 @@ fn leaf(matches: &ArgMatches) -> (Vec, &ArgMatches) { #[cfg(test)] mod tests { use super::*; + use crate::config::CredentialProblem as Problem; use crate::error::ErrorKind as Kind; use std::path::Path; @@ -1850,6 +1851,44 @@ mod tests { } } + #[test] + fn troubleshooting_covers_every_exit_code_and_credential_problem() { + let text = std::fs::read_to_string( + PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("docs/guides/troubleshooting.md"), + ) + .unwrap() + // Git may check the file out with CRLF line endings, as on Windows. + .replace("\r\n", "\n"); + for code in error_kinds().map(Kind::exit_code) { + assert!( + text.contains(&format!("\n### Exit code {code}\n")), + "exit code {code}" + ); + } + // A new problem fails to compile here until it has a name, but its entry is checked + // only once it is also added to the list below. + let name = |problem: Problem| match problem { + Problem::EmptyApiKey => "empty_api_key", + Problem::NoCredential => "no_credential", + Problem::MultipleCredentials => "multiple_credentials", + Problem::UnknownCredential => "unknown_credential", + Problem::BoundElsewhere => "bound_elsewhere", + Problem::AuthUrlMismatch => "auth_url_mismatch", + }; + for problem in [ + Problem::EmptyApiKey, + Problem::NoCredential, + Problem::MultipleCredentials, + Problem::UnknownCredential, + Problem::BoundElsewhere, + Problem::AuthUrlMismatch, + ] { + let name = name(problem); + assert_eq!(serde_json::to_value(problem).unwrap(), name); + assert!(text.contains(&format!("\n### `{name}`\n")), "{name}"); + } + } + /// Every command below `cmd`, by path, with its rendered long help. fn long_helps(cmd: &mut ClapCommand, path: &mut Vec, out: &mut Vec<(String, String)>) { out.push((path.join(" "), cmd.render_long_help().to_string())); diff --git a/tests/cli_integration.rs b/tests/cli_integration.rs index 022786c..4b4c0f0 100644 --- a/tests/cli_integration.rs +++ b/tests/cli_integration.rs @@ -4248,7 +4248,7 @@ fn file_names(dir: &Path) -> BTreeSet { async fn documented_changes_pass_as_offline_dry_runs() { let (server, dir) = fixture().await; let mut settings = seeded_api_key(dir.path(), "key", ACCOUNT_KEY); - for name in ["prod", "staging"] { + for name in ["prod", "staging", "test"] { settings.config.profiles.insert( name.into(), Profile { diff --git a/tests/support/examples.rs b/tests/support/examples.rs index bb75738..66717cc 100644 --- a/tests/support/examples.rs +++ b/tests/support/examples.rs @@ -4,13 +4,19 @@ use std::path::PathBuf; -/// The README and the docs index, guides, concepts, reference, and development guide, as -/// (path from the repository root, contents). +/// The README, changelog, docs index, tutorial, guides, concepts, reference, and development +/// guide, as (path from the repository root, contents). pub fn user_docs() -> Vec<(PathBuf, String)> { let root = PathBuf::from(env!("CARGO_MANIFEST_DIR")); - let mut paths = ["README.md", "docs/README.md", "docs/development.md"] - .map(|path| root.join(path)) - .to_vec(); + let mut paths = [ + "README.md", + "CHANGELOG.md", + "docs/README.md", + "docs/getting-started.md", + "docs/development.md", + ] + .map(|path| root.join(path)) + .to_vec(); for dir in ["guides", "concepts", "reference"] { for entry in std::fs::read_dir(root.join("docs").join(dir)).unwrap() { let path = entry.unwrap().path();