Checkpoint Lab — GitHub CLI, gh Authentication, Repository Operations, and Scripting Workflows
The checkpoint assembles the chapter into a small operational artifact: a repository report that can run in a human shell or CI without guessing context, prompting, exposing secrets, or hiding failures. You will also prove that your error path behaves predictably when authorization is denied.
Learning objectives
- Build a read-only repository-report script that returns repository metadata, open Issues, and open pull requests as structured JSON.
- Make host/repository context, prompt behavior, paging, output, and exit codes deterministic enough for CI.
- Verify independently that the report targets the intended repository and does not mutate GitHub state.
- Exercise a sanitized authorization-denied fixture and optionally confirm the same control path with a short-lived least-privilege token.
- Write an operator handoff that states inputs, outputs, failure codes, security constraints, and cleanup.
1. Checkpoint scenario and assumptions
You are preparing a small repository-report job for CI. It must identify one repository, return repository metadata plus open Issues and pull requests, never prompt, never print credentials, and use predictable status codes. Its default behavior is entirely read-only.
| Requirement | Assumption / proof |
|---|---|
| Repository |
Explicit OWNER/REPO; prove with
gh repo view ... --json nameWithOwner.
|
| Host |
Explicit github.com by default; script accepts
another host only when intentionally supplied.
|
| Authentication |
Existing safe gh login for human lab; CI would
inject a job-scoped token such as GH_TOKEN.
|
| Authorization | Read access is sufficient for the report. Optional live permission-failure rehearsal uses a separate short-lived, intentionally underprivileged fine-grained PAT. |
| Mutations | None in report mode. Cleanup mutations are explicit and confined to the disposable lab. |
2. Predict before execution
Write these predictions in your local lab note before running the script:
- Prediction A: running the report changes no Git ref, Issue, pull request, repository setting, workflow, package, or API object; it only performs authenticated reads and writes JSON to stdout.
- Prediction B: moving to another local Git directory does not change the report target because the repository is supplied explicitly.
- Prediction C: an authorization-denied result causes a non-zero report status and no mutation; the script never retries the denied operation as a write.
After each phase, verify the prediction independently with
gh repo view, Issue/PR list state, and local Git refs.
3. Build the Bash/Git Bash repository-report script
Create repo-report.sh. This version maps failures to
documented wrapper codes while preserving original CLI stderr. It
never uses set -x and never reads/prints a token value.
#!/usr/bin/env bash
set -u
REPO="${1:-}"
HOST="${2:-github.com}"
# Wrapper exit contract:
# 0 success, 64 usage, 65 auth, 66 repo read, 67 issue read, 68 PR read
if [[ -z "$REPO" ]]; then
echo "usage: repo-report.sh OWNER/REPO [HOST]" >&2
exit 64
fi
export GH_PROMPT_DISABLED=1
export GH_HOST="$HOST"
export GH_REPO="$REPO"
export NO_COLOR=1
if ! gh auth status --active --hostname "$HOST" >/dev/null 2>&1; then
echo "authentication preflight failed for host=$HOST" >&2
exit 65
fi
if ! repo_json="$(gh repo view "$REPO" \
--json nameWithOwner,visibility,defaultBranchRef,isArchived,url,viewerPermission)"; then
echo "repository read failed for $HOST/$REPO" >&2
exit 66
fi
if ! issues_json="$(gh issue list -R "$REPO" --state open --limit 100 \
--json number,title,updatedAt,url)"; then
echo "issue read failed for $HOST/$REPO" >&2
exit 67
fi
if ! prs_json="$(gh pr list -R "$REPO" --state open --limit 100 \
--json number,title,isDraft,headRefName,baseRefName,updatedAt,url)"; then
echo "pull-request read failed for $HOST/$REPO" >&2
exit 68
fi
# Command substitution is non-TTY; these are JSON values. Keep them quoted.
printf '{"repository":%s,"openIssues":%s,"openPullRequests":%s}\n' \
"$repo_json" "$issues_json" "$prs_json"
Run it and capture status separately from the JSON:
./repo-report.sh "$REPO" github.com > repo-report.json
status=$?
printf 'exit=%s\n' "$status"
python -m json.tool repo-report.json
python -m json.tool? It
validates/presents the final JSON in the lab. The report itself does
not depend on Python. In a production consumer, use the
language/runtime already responsible for the next processing step.
4. PowerShell equivalent for Windows-native CI
The same contract can be expressed without Bash. PowerShell must
check $LASTEXITCODE immediately after each external
gh invocation. Do not rely on exceptions alone for
native-process failures in all PowerShell configurations.
param(
[Parameter(Mandatory=$true)][string]$Repo,
[string]$HostName = "github.com"
)
$env:GH_PROMPT_DISABLED = "1"
$env:GH_HOST = $HostName
$env:GH_REPO = $Repo
$env:NO_COLOR = "1"
gh auth status --active --hostname $HostName *> $null
if ($LASTEXITCODE -ne 0) {
Write-Error "authentication preflight failed for host=$HostName"
exit 65
}
$repoJson = gh repo view $Repo --json nameWithOwner,visibility,defaultBranchRef,isArchived,url,viewerPermission
if ($LASTEXITCODE -ne 0) { exit 66 }
$issuesJson = gh issue list -R $Repo --state open --limit 100 --json number,title,updatedAt,url
if ($LASTEXITCODE -ne 0) { exit 67 }
$prsJson = gh pr list -R $Repo --state open --limit 100 --json number,title,isDraft,headRefName,baseRefName,updatedAt,url
if ($LASTEXITCODE -ne 0) { exit 68 }
[ordered]@{
repository = ($repoJson | ConvertFrom-Json)
openIssues = @($issuesJson | ConvertFrom-Json)
openPullRequests = @($prsJson | ConvertFrom-Json)
} | ConvertTo-Json -Depth 8
exit 0
Run:
.\repo-report.ps1 -Repo $REPO | Set-Content
repo-report.json, then
Get-Content repo-report.json | ConvertFrom-Json.
5. Verify target and read-only behavior independently
Do not trust the report merely because it returned valid JSON. Compare it with independent GitHub and Git inspection:
gh repo view "$REPO" --json nameWithOwner,url,defaultBranchRef,viewerPermission
gh issue list -R "$REPO" --state open --json number,title,url
gh pr list -R "$REPO" --state open --json number,title,headRefOid,url
# Local Git evidence: report execution should not change refs
git status --short
git show-ref --heads
-
The report’s
repository.nameWithOwnerequals the explicitREPOargument. - Open Issue/PR counts and identifiers agree with independent first-class queries.
- No new local commits/branches appear because the report is read-only.
- No Issue title/state, PR head SHA, repository setting, or workflow state changes as a result of report execution.
6. Prove that current-directory context cannot redirect the report
Change to a different Git repository (any safe local clone), then run the report with the same explicit argument:
cd /path/to/another/local/repository
./path/to/repo-report.sh "$REPO" github.com > second-report.json
# The reported nameWithOwner must still be the explicit target.
python - <<'PY'
import json
print(json.load(open('second-report.json'))['repository']['nameWithOwner'])
PY
This verifies Prediction B. If the target changes, the script still contains inferred repository context somewhere and is not CI-safe.
7. Test an intentional permission failure safely
Mandatory no-new-credential test: exercise the report’s permission/error classifier with a sanitized 403 fixture. This proves that the CI control path fails closed without creating or exposing a real token.
cat > forbidden-fixture.json <<'EOF'
{
"status": 403,
"message": "Resource not accessible by personal access token",
"documentation_url": "https://docs.github.com/rest"
}
EOF
python - <<'PY'
import json, sys
x=json.load(open('forbidden-fixture.json'))
if x.get('status') == 403:
print('authorization denied: do not retry or broaden blindly', file=sys.stderr)
raise SystemExit(77)
raise SystemExit(0)
PY
printf 'fixture exit=%s (expected 77)\n' "$?"
GH_TOKEN; attempt an Issue edit that
should be denied; capture the sanitized 403/error and exit status;
then immediately unset GH_TOKEN and revoke the token.
Never print, paste, store, commit, screenshot, or send the token. If
it is exposed, revoke/rotate first.
The optional live test validates the same conceptual boundary: authentication can succeed while authorization for a specific mutation fails. The correct response is not “add every scope”; it is to decide whether that mutation belongs in the automation and grant only the minimal permission if justified.
8. Add one versioned raw API read as an audit cross-check
Use gh api as an independent view of repository
identity—not as a replacement for the first-class report:
gh api \
-H "Accept: application/vnd.github+json" \
-H "X-GitHub-Api-Version: 2026-03-10" \
"repos/$REPO" \
--jq '{full_name,visibility,default_branch,archived}'
For a larger inventory, add --paginate only to
endpoints that paginate and handle rate limits explicitly. Do not
add pagination flags as decoration to single-resource GETs.
9. Operator handoff policy
Write a short REPO_REPORT_RUNBOOK.md beside the script
with these rules:
-
Inputs: supported host and explicit
OWNER/REPO; no implicit current-directory target. - Authentication: human uses secure stored login; CI injects a job-scoped token. Never enable token-display/debug logging in routine operation.
- Authorization: report needs read-only repository/Issues/PR visibility. Any future mutation is a separate reviewed mode with least privilege and idempotence design.
- Outputs: stdout is JSON only on success; diagnostics go to stderr; exit-code mapping is documented.
- Extensions/aliases: report depends only on core GitHub CLI commands. Third-party extensions are not part of the trusted runtime unless separately reviewed/pinned.
- Failure handling: no blind retry on authentication, permission, validation, or policy errors; preserve sanitized evidence and classify first.
- Secrets: never log token values or dump the full environment. Exposed credential response begins with revoke/rotate.
10. Cleanup and rollback
The report created no hosted state, so its cleanup is local: remove
repo-report.json, fixtures, and scripts if you do not
want to keep them. The Chapter 12 repository itself is disposable;
preserve any screenshots/log snippets with credentials excluded,
then close the test Issue/PR and archive the repository through the
UI or CLI as a reversible endpoint.
# Close disposable work first
gh issue close "$ISSUE_NUMBER" -R "$REPO" --comment "Chapter 12 lab complete."
gh pr close "$PR_URL" -R "$REPO" --delete-branch
# Reversible repository cleanup
gh repo archive "$REPO" --yes
# Verify hosted state
gh repo view "$REPO" --json nameWithOwner,isArchived,url
11. Lesson summary
The checkpoint produced a CI-oriented, read-only repository report with explicit target context, structured output, predictable wrapper exit codes, separate stdout/stderr, no secret logging, and a tested authorization-denied path. Those are the same operational properties GitHub Actions jobs will need when Chapter 13 introduces workflow events, YAML, job permissions, and runner execution.
Knowledge check
Why can the report run from any local directory without changing target?
Repository and host are explicit inputs and are passed/set for every GitHub operation; it does not infer the target from the current clone.
What proves the report is read-only?
Independent before/after inspection shows the same Git refs, Issue/PR states, repository settings, and hosted object identities; the script contains only read commands.
The optional fine-grained token can read the repo but an Issue edit returns 403. Is authentication broken?
Not necessarily. This is the intended demonstration of successful authentication with insufficient authorization for the specific mutation.
Why should stdout contain JSON while diagnostics go to stderr?
It gives downstream programs a clean machine-readable data stream while preserving human/operator error evidence separately.
What is the chapter’s bridge to GitHub Actions?
Chapter 12 establishes deterministic CLI/API behavior—explicit context, structured outputs, exit codes, least privilege, and no secret logging—which Chapter 13 will place inside event-driven workflow jobs and runner execution.
Further reading — current official GitHub sources
- GitHub CLI manual — environment variables
- GitHub CLI manual — authentication status
- GitHub CLI manual — authentication login
- GitHub CLI manual — formatting
- GitHub CLI manual — exit codes
- GitHub CLI manual — gh api
- GitHub CLI manual — configuration
- GitHub CLI manual — aliases
- GitHub CLI manual — extensions
- GitHub REST API — API versions
- GitHub REST API — rate limits
- GitHub CLI repository — installation guidance
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.
0x716c4Ab160C4B66F31a28AE2448BfF68fc3a2ef0
Send only Ethereum/ERC-20 compatible assets to this
address.