Checkpoint Lab — REST API, GraphQL API, Webhooks, System Hooks, Pagination, Rate Limits, and Automation
Build and validate a small resilient automation workflow that paginates reads, handles GraphQL cursors/errors, verifies and deduplicates a signed webhook, performs an idempotent mutation, and cleans up safely.
Learning objectives
- Build a small automation checkpoint that reads complete GitLab state and preserves sanitized HTTP evidence.
- Paginate GraphQL with cursors and explicitly inspect errors.
- Authenticate, replay, and tamper-test a synthetic signed webhook locally.
- Run the same desired-state mutation twice and prove duplicate safety.
- Clean up the disposable mutation/credentials and document a production runbook.
glab api, and project webhooks have
Free-compatible paths across GitLab.com, Self-Managed, and Dedicated.
Group webhooks require Premium/Ultimate. System hooks
are instance-wide administrator controls documented for Self-Managed
and Dedicated, while the current System Hooks REST API reference is
Self-Managed-specific. New webhooks should prefer the GitLab 19.1+
HMAC-SHA256 signing-token mechanism. The mandatory chapter path uses a
GitLab Free disposable project, a local synthetic receiver, and one
harmless label mutation; it does not require a public webhook
endpoint, paid tier, administrator access, or production token.
1. Checkpoint scenario and preflight
Your team is building an integration worker for a disposable project. It needs a complete project inventory read, an issue read model, fast event intake, and one managed marker label. The mandatory exercise remains GitLab Free and local-only for webhook delivery. Use a project where deleting the lab label cannot affect real automation.
glab auth status
glab api projects/:fullpath | jq '{id,path_with_namespace,visibility}'
mkdir -p ch29-checkpoint/evidence && cd ch29-checkpoint
automation-managed, and no token/signing key will be
written to evidence or Git.
2. Predict four state transitions before execution
- REST pagination will retrieve every label even if the first page contains only two.
-
GraphQL continuation will stop only when
hasNextPage=false. - Replaying the same authenticated webhook ID will produce no second downstream action.
- Running the label reconciler twice will leave exactly one desired label and the second run will be a no-op.
Write these predictions to evidence/predictions.md.
3. REST read and sanitized response evidence
glab api -i "projects/:fullpath/labels?per_page=2" > evidence/labels-first-page.txt
# Remove any accidental authorization-related header before retaining evidence.
sed -i.bak -E '/^(PRIVATE-TOKEN|Authorization|JOB-TOKEN):/Id' evidence/labels-first-page.txt
rm -f evidence/labels-first-page.txt.bak
glab api projects/:fullpath/labels --paginate --output ndjson | jq '{id,name,color}' > evidence/labels-all.ndjson
Record the project ID/path and observed pagination mechanism. If you
implement this outside glab, follow
Link rel="next" exactly; do not invent the next URL.
4. GraphQL cursor evidence
glab api graphql --paginate --output ndjson -f fullPath='training/api-automation-lab' -f query='query($fullPath: ID!, $endCursor: String) {
project(fullPath: $fullPath) {
issues(first: 2, after: $endCursor) {
nodes { iid title state }
pageInfo { hasNextPage endCursor }
}
}
}' > evidence/issues.ndjson
# Inspect error fields if your wrapper stores the full GraphQL envelopes.
Do not store more issue text than the exercise needs. Production logging should use identifiers/status and redact fields that can contain confidential issue content.
5. Signed webhook: accept, replay, then tamper
Reuse the verified receiver.py and
send_webhook.py from Lesson 2. Generate a fresh
ephemeral signing token and fixed event ID. Send the same signed
event twice: one process action, one duplicate no-op. Then create a
negative test by changing one body byte after calculating
the signature; the receiver must return 401 and perform no action.
export WEBHOOK_SIGNING_TOKEN="$(python - <<'PYCODE'
import base64, os
print('whsec_' + base64.b64encode(os.urandom(32)).decode())
PYCODE
)"
export WEBHOOK_ID='ch29-checkpoint-001'
# terminal 1: python receiver.py
# terminal 2: python send_webhook.py; python send_webhook.py
# Expected receiver evidence: duplicate false, then true.
# Negative test: mutate the exact body after signing; expected HTTP 401.
Preserve only event ID, timestamps, verification result, duplicate result, and selected event kind. Never save the signing token.
6. Desired-state label: converge twice
#!/usr/bin/env bash
set -euo pipefail
LABEL="automation-managed"
DESC="Chapter 29 disposable automation marker"
COLOR="#428BCA"
current="$(glab api projects/:fullpath/labels --paginate --output ndjson | jq -s --arg n "$LABEL" '[.[] | select(.name == $n)] | first')"
if [ "$current" = "null" ]; then
echo "create: label absent"
glab api --method POST projects/:fullpath/labels -f name="$LABEL" -f color="$COLOR" -f description="$DESC" >/dev/null
else
id="$(jq -r '.id' <<<"$current")"
actual="$(jq -r '.description // ""' <<<"$current")"
if [ "$actual" != "$DESC" ]; then
echo "update: label exists but differs"
glab api --method PUT "projects/:fullpath/labels/$id" -f description="$DESC" -f color="$COLOR" >/dev/null
else
echo "no-op: desired state already present"
fi
fi
glab api projects/:fullpath/labels --paginate --output ndjson | jq --arg n "$LABEL" 'select(.name == $n) | {id,name,color,description}'
chmod +x ensure-label.sh
./ensure-label.sh | tee evidence/label-run-1.txt
./ensure-label.sh | tee evidence/label-run-2.txt
glab api projects/:fullpath/labels --paginate --output ndjson | jq -s '[.[] | select(.name=="automation-managed")] | {count:length,items:.}' | tee evidence/label-final.json
# PASS requires count == 1 and the second run to be no-op.
7. Write the retry policy before productionizing
| Condition | Checkpoint rule |
|---|---|
| 2xx read | Process and continue pagination. |
| 401/403 | Stop; diagnose identity/role/scope. Do not retry. |
| 404 | Confirm path/resource visibility before assuming absence. |
| 409 | Read current state; decide whether conflict already represents desired state. |
| 429 | Honor Retry-After, add jitter, reduce concurrency; re-read before writes. |
| 5xx/network failure on GET | Bounded exponential retry with jitter. |
| 5xx/network failure on mutation | Treat outcome as uncertain; read current state before replay. |
8. Optional real webhook and system-hook governance
If you already have a private, disposable, HTTPS receiver you control, a project Maintainer/Owner may optionally create a project webhook and select only the needed events. Prefer a signing token, test once, capture the hook/delivery IDs, then delete the hook by ID and verify it is absent. The mandatory path never requires a public tunnel.
For system hooks, write a governance fixture rather than changing an instance: owner = platform administrators; events = only required instance lifecycle events; signing token stored in a secret manager; receiver ACL/monitoring; rotation runbook; retention of sanitized delivery IDs/status; documented Self-Managed/Dedicated/API support assumption.
9. Cleanup and residual-state verification
Delete only the checkpoint label and any optional hook/token you explicitly created. Then independently verify absence.
id="$(glab api projects/:fullpath/labels --paginate --output ndjson | jq -r 'select(.name == "automation-managed") | .id' | head -n1)"
[ -n "$id" ] && glab api --method DELETE "projects/:fullpath/labels/$id" >/dev/null
if glab api projects/:fullpath/labels --paginate --output ndjson | jq -e 'select(.name == "automation-managed")' >/dev/null; then
echo 'FAIL: lab label remains' >&2; exit 1
fi
unset WEBHOOK_SIGNING_TOKEN WEBHOOK_ID
# Scan evidence for obvious credential patterns before retaining it.
if grep -RniE 'PRIVATE-TOKEN:|Authorization: Bearer|JOB-TOKEN:|whsec_[A-Za-z0-9+/=]{20,}' evidence; then
echo 'FAIL: review evidence for credential leakage' >&2; exit 1
fi
echo 'PASS: mutation removed and evidence scan clean'
10. Verification checklist
-
REST collection completion uses
--paginateor server-provided next links. - GraphQL query accepts/uses a cursor and errors are checked.
- Webhook HMAC and timestamp are verified before parse/action.
- Same webhook ID is harmless on replay; tampered payload is rejected.
- Same desired-state label automation run twice leaves one object.
- 429/write-uncertainty policy distinguishes reads from mutations.
- Evidence contains resource/request/event IDs and outcomes but no credentials.
- Disposable mutation and optional integration resources are removed and read-back verified.
11. What Chapter 29 adds to the production GitLab operating model
GitLab is now an integration platform in your model, not just a UI/CI host. Production automation has explicit host/project identity, least-privilege credentials, complete pagination, GraphQL error semantics, authenticated/deduplicated event intake, bounded rate-limit handling, desired-state mutation, and audit-friendly evidence. Chapter 30 moves to Self-Managed administration—configuration, email, object storage, backup, restore, and maintenance—where automation must respect an even larger blast radius.
Knowledge check
What proves REST list completeness?
Following the documented continuation mechanism until no next link/cursor remains—not a successful first page or an assumed total header.
Why replay the same webhook ID?
To prove duplicate delivery does not repeat the downstream side effect.
Why is a tampered-body test important?
It proves the receiver authenticates the exact raw bytes and does not merely accept a plausible event shape.
What proves the label mutation is idempotent?
Two runs converge to exactly one label with the desired fields, and the second run performs no write.
Should a 429 on a mutation trigger the same retry as a GET?
No. Wait/back off, then re-read state before deciding whether a write must be attempted again.
What is the Chapter 30 bridge?
Self-Managed GitLab administration: configuration, email, object storage, backups, restore, and maintenance with administrator-level safety.
12. Chapter close
You have completed the GitLab automation foundation: documented machine interfaces, robust collection traversal, verified event intake, and duplicate-safe mutations. Keep these invariants as the course moves into administrator-controlled infrastructure.
Primary sources and version notes
These lessons were finalized against current official GitLab documentation on 2026-08-22. API fields, rate limits, webhook event schemas, CLI flags, and tier/offering availability can change, so production clients should pin/document assumptions and re-check the API/CLI documentation for the deployed GitLab version.
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.
0x716c4Ab160C4B66F31a28AE2448BfF68fc3a2ef0Send only Ethereum/ERC-20 compatible assets to this
address.