gh-safe: try gh first, fall back to gh api
Why this exists
gh pr edit --body "..." (and similar write commands) can fail with:
error: your authentication token is missing required scopes [read:project]
To request it, run: gh auth refresh -s read:project
The suggested gh auth refresh requires interactive browser confirmation, which
blocks automation. The gh api REST equivalent works with the existing token
and does not need the extra scope. So the right response is to fall back
immediately — not to ask the user to re-auth.
The pattern
- Run the high-level
ghcommand and capture stderr. - If exit code is non-zero and stderr contains
"missing required scopes"or"authentication token", retry using thegh apiequivalent below. - Do not prompt the user to re-authenticate or run
gh auth refresh.
# Example: update a PR body
if ! gh pr edit 123 --repo owner/repo --body "text" 2>/dev/null; then
# Fallback
gh api repos/owner/repo/pulls/123 --method PATCH --field body='text'
fi
Resolve owner/repo
When the repo is not already known, resolve it once:
REPO=$(gh repo view --json owner,name -q '"\(.owner.login)/\(.name)"')
# e.g. blockscout/mcp-server
Command → API fallback table
Pull requests
| High-level command | gh api fallback |
|---|---|
gh pr edit <n> --body "..." |
gh api repos/{R}/pulls/<n> --method PATCH --field body='...' |
gh pr edit <n> --title "..." |
gh api repos/{R}/pulls/<n> --method PATCH --field title='...' |
gh pr edit <n> --add-label "..." |
gh api repos/{R}/issues/<n>/labels --method POST --field 'labels[]=...' |
gh pr create --title T --body B |
gh api repos/{R}/pulls --method POST --field title='T' --field body='B' --field head='<branch>' --field base='main' |
gh pr comment <n> --body "..." |
gh api repos/{R}/issues/<n>/comments --method POST --field body='...' |
gh pr close <n> |
gh api repos/{R}/pulls/<n> --method PATCH --field state='closed' |
gh pr merge <n> --squash |
gh api repos/{R}/pulls/<n>/merge --method PUT --field merge_method='squash' |
Issues
| High-level command | gh api fallback |
|---|---|
gh issue edit <n> --body "..." |
gh api repos/{R}/issues/<n> --method PATCH --field body='...' |
gh issue edit <n> --title "..." |
gh api repos/{R}/issues/<n> --method PATCH --field title='...' |
gh issue create --title T --body B |
gh api repos/{R}/issues --method POST --field title='T' --field body='B' |
gh issue comment <n> --body "..." |
gh api repos/{R}/issues/<n>/comments --method POST --field body='...' |
gh issue close <n> |
gh api repos/{R}/issues/<n> --method PATCH --field state='closed' |
Releases
| High-level command | gh api fallback |
|---|---|
gh release create <tag> --title T --notes N |
gh api repos/{R}/releases --method POST --field tag_name='<tag>' --field name='T' --field body='N' |
gh release edit <tag> --notes N |
First gh api repos/{R}/releases/tags/<tag> -q .id to get the numeric ID, then gh api repos/{R}/releases/<id> --method PATCH --field body='N' |
{R} = owner/repo resolved above.
Tips
--fieldURL-encodes the value; use--raw-fieldwhen the value must be passed byte-for-byte (rare).- Multi-line bodies work cleanly with a HEREDOC:
gh api repos/{R}/pulls/123 --method PATCH --field body="$(cat <<'EOF' ## Summary ... EOF )" gh apireturns JSON. Append-q .html_url(or any jq path) to extract just what you need.- The
--repoflag on high-level commands is equivalent to resolving{R}manually forgh api; either approach is fine.