Chapter 26Lesson 05~230 minutes

Checkpoint Lab — REST API, GraphQL API, gh api, Pagination, Rate Limits, and Automation Clients

Build a complete read-only inventory client with REST and GraphQL pagination, rate-limit/error handling, structured evidence, and one dry-run-gated idempotent mutation against a disposable issue, then hand off a production automation policy.

CheckpointInventoryRate handlingIdempotenceRunbook

Checkpoint objectives

  • Produce a complete structured inventory through explicit REST and GraphQL pagination.
  • Capture safe rate/error evidence and reject partial GraphQL results.
  • Predict dry-run and applied mutation state before execution.
  • Prove a desired-state mutation cannot create duplicate marker issues across repeated runs.
  • Write a production API-client handoff policy and bridge to Apps/webhooks in Chapter 27.
Assumptions: GitHub.com + GitHub Free + a disposable public personal repository; GitHub CLI already authenticated; Python 3, Git, curl. No organization, paid feature, token creation, App registration, runner, or policy bypass is required.

1. Scenario and predictions

Create atlas-c26-checkpoint with seven synthetic issues. Before running the client, write these predictions:

Prediction Expected state Proof
P1 per_page=2 requires multiple REST pages. Paginator page count and final 7-item inventory.
P2 GraphQL cursor traversal returns the same seven issues. Normalize/sort REST and GraphQL tuples.
P3 Default client run makes zero mutations. Hosted issue IDs/count unchanged after dry-run.
P4 First --apply creates one marker; second creates none. Exactly one durable marker in state=all.

2. Setup and preflight

gh auth status
OWNER="$(gh api -H 'X-GitHub-Api-Version: 2026-03-10' user --jq .login)"
REPO="$OWNER/atlas-c26-checkpoint"
gh repo create "$REPO" --public --clone --description "Disposable Chapter 26 checkpoint"
cd atlas-c26-checkpoint
printf '# Chapter 26 Checkpoint
' > README.md
git add README.md && git commit -m "Initialize checkpoint" && git push -u origin HEAD
for n in 1 2 3 4 5 6 7; do
  gh issue create --title "checkpoint-inventory-$n" --body "chapter26-checkpoint-fixture-$n"
done
git rev-parse HEAD
gh repo view "$REPO" --json id,nameWithOwner,visibility,defaultBranchRef

3. Build the checkpoint inventory client

Create checkpoint_client.py. It uses gh api as the transport, rejects GraphQL errors, compares normalized REST/GraphQL inventories, captures safe rate data, and plans the mutation unless --apply is explicit.

#!/usr/bin/env python3
import argparse, json, subprocess
VERSION="2026-03-10"
MARKER="chapter26-checkpoint-mutation-v1"

def run(args, input_text=None):
    p=subprocess.run(args,input=input_text,text=True,capture_output=True)
    if p.returncode:
        raise RuntimeError(f"rc={p.returncode}: {p.stderr[:500]}")
    return p.stdout

def rest_all(repo):
    pages=json.loads(run(["gh","api","--paginate","--slurp",
        "-H",f"X-GitHub-Api-Version: {VERSION}",
        f"repos/{repo}/issues?state=all&per_page=2"]))
    return pages,[x for page in pages for x in page if "pull_request" not in x]

def graphql_all(owner,name):
    q="""query($owner:String!,$name:String!,$endCursor:String){
      repository(owner:$owner,name:$name){issues(first:2,after:$endCursor,orderBy:{field:CREATED_AT,direction:ASC}){
        nodes{number title state body} pageInfo{hasNextPage endCursor}}}
      rateLimit{cost remaining resetAt}}
"""
    pages=json.loads(run(["gh","api","graphql","--paginate","--slurp",
        "-F",f"owner={owner}","-F",f"name={name}","-f",f"query={q}"]))
    items=[]
    for page in pages:
        if page.get("errors"):
            raise RuntimeError(f"GraphQL errors: {page['errors']}")
        items.extend(page["data"]["repository"]["issues"]["nodes"])
    return pages,items

def rate():
    core=json.loads(run(["gh","api","-H",f"X-GitHub-Api-Version: {VERSION}","rate_limit"]))["resources"]["core"]
    return {k:core[k] for k in ("limit","remaining","used","reset")}

def ensure(repo,items,apply):
    e=[i for i in items if MARKER in (i.get("body") or "")]
    if e: return {"action":"none","reason":"marker-exists","number":e[0]["number"]}
    if not apply: return {"dry_run":True,"action":"create-issue","marker":MARKER}
    body=json.dumps({"title":"Chapter 26 checkpoint mutation","body":MARKER})
    c=json.loads(run(["gh","api","-X","POST","-H",f"X-GitHub-Api-Version: {VERSION}",
        f"repos/{repo}/issues","--input","-"],body))
    return {"dry_run":False,"action":"created","number":c["number"],"id":c["id"]}

p=argparse.ArgumentParser(); p.add_argument("repo"); p.add_argument("--apply",action="store_true")
a=p.parse_args(); owner,name=a.repo.split("/",1)
rp,ri=rest_all(a.repo); gp,gi=graphql_all(owner,name)
rnorm=sorted((i["number"],i["title"],i["state"]) for i in ri)
gnorm=sorted((i["number"],i["title"],i["state"]) for i in gi)
if rnorm!=gnorm: raise RuntimeError("REST/GraphQL inventories differ")
print(json.dumps({"repo":a.repo,"rest_pages":len(rp),"graphql_pages":len(gp),
 "issue_count":len(ri),"rate":rate(),"mutation":ensure(a.repo,ri,a.apply)},indent=2))

4. Verify P1–P3: complete inventory and zero-change dry-run

python checkpoint_client.py "$REPO" | tee checkpoint-dry-run.json
gh issue list --repo "$REPO" --state all --limit 100 --json number,title,state,body > issues-before-apply.json
python - <<'PY'
import json
r=json.load(open('checkpoint-dry-run.json'))
assert r['issue_count']==7
assert r['rest_pages']>=4
assert r['graphql_pages']>=4
assert r['mutation']['dry_run'] is True
print('P1/P2/P3 accepted')
PY

The JSON evidence is synthetic/public here. In production, repository inventories may be sensitive; do not automatically commit or upload them.

5. Verify P4: one mutation and convergence

python checkpoint_client.py "$REPO" --apply | tee checkpoint-apply-1.json
python checkpoint_client.py "$REPO" --apply | tee checkpoint-apply-2.json

gh api --paginate --slurp   -H "X-GitHub-Api-Version: 2026-03-10"   "repos/$REPO/issues?state=all&per_page=2" > all-pages.json
python - <<'PY'
import json
pages=json.load(open('all-pages.json'))
items=[x for p in pages for x in p if 'pull_request' not in x]
markers=[x for x in items if 'chapter26-checkpoint-mutation-v1' in (x.get('body') or '')]
assert len(markers)==1, len(markers)
print('marker_issue=',markers[0]['number'])
PY

Expected final count: eight issues—seven fixtures plus one marker. If two markers exist, preserve both IDs and diagnose the duplicate guard before cleanup.

6. Failure injection without rate abuse

Do not hammer the service. Inject a deterministic GraphQL schema failure and prove the client would reject error-bearing results:

set +e
gh api graphql -f query='query { viewer { definitelyNotAField } }' >graphql-broken.json 2>graphql-broken.err
rc=$?
set -e
printf 'exit_code=%s
' "$rc"
sed -n '1,40p' graphql-broken.err

Restore the valid query afterward. The point is to interpret the failure, not to hide it.

7. Deployment/operations gate for API automation

Concept / workflow diagram
              flowchart TD
                E["Webhook / schedule / operator"] --> I["Scoped identity"]
                I --> Q["Complete read"]
                Q --> V{"Version + errors + pages + rate OK?"}
                V -->|no| S["Stop / backoff / investigate"]
                V -->|yes| D["Desired-state diff / dry-run"]
                D --> M{"Mutation needed?"}
                M -->|no| O["No-op evidence"]
                M -->|yes| G["Duplicate guard / precondition"]
                G --> W["Bounded mutation"]
                W --> P["Read-after-write proof"]
            

Your runbook must name supported hosts/versions, identity/permissions, endpoints/fields, pagination, partial-result behavior, rate/backoff, webhook-vs-polling policy, dry-run default, duplicate key/preconditions, request-ID/status/page metrics, redaction, and incident owner.

8. Cleanup/rollback

gh issue list --repo "$REPO" --state open --limit 100 --json number --jq '.[].number' | while read -r n; do
  gh issue close "$n" --repo "$REPO" --reason completed
done
gh repo archive "$REPO" --yes

Repository deletion is destructive and optional. Archive retains evidence and prevents ordinary writes.

9. Verification checklist

  • Seven fixtures existed before mutation.
  • REST and GraphQL both required multiple pages.
  • Healthy GraphQL path contained no errors and normalized inventory matched REST.
  • Dry-run changed no hosted resource.
  • Two applied runs left exactly one marker issue.
  • Rate evidence contained no credential.
  • No create path blindly retries after ambiguity.
  • Cleanup archived the disposable repository and closed all issues.

Knowledge check

A dry-run says “would create.” What independently proves it did not mutate?

REST and GraphQL inventories differ by one issue. Continue to mutation?

Why is a durable body marker stronger than “the previous run succeeded”?

Primary remaining is zero. What controls retry?

Why does Chapter 27 naturally follow this checkpoint?

Production operating-model addition

Chapter 26 adds a governed API-client layer to the production GitHub operating model: explicit host/version/identity, complete pagination, partial-result rejection, rate-aware backoff, dry-run desired-state planning, duplicate-resistant mutations, and read-after-write evidence. Chapter 27 turns that discipline into long-running Apps and event-driven integrations.

Next chapter

GitHub Apps, OAuth Apps, Webhooks, Checks API, and Event-Driven Integrations: Concepts, Architecture, and Mental Model

Official references

Keep the academy open

Support free, practical DevOps education.

Every lesson is designed to remain readable in a browser, downloadable from GitHub, and usable without a paid learning platform. Contributions help expand and maintain the curriculum.

Ethereum / ERC-20
0x716c4Ab160C4B66F31a28AE2448BfF68fc3a2ef0 Send only Ethereum/ERC-20 compatible assets to this address.