PHASE 12 OF 14
GitHub CLI & API Automation
Master the gh CLI — auth, scripting, --json/--jq pipelines — then graduate to the REST API v3, GraphQL API v4, Octokit SDK, and GitHub Apps. Build practical automations: auto-assign reviewers, stale issue closers, release notifiers, and org onboarding bots. Finish with Actions + GraphQL to drive Projects v2 from CI.
GitHub CLI
REST API
GraphQL
Octokit
GitHub Apps
Webhooks
Automation
12.1
gh CLI: Authentication, Config, Extensions & Aliases
The GitHub CLI (gh) is the official command-line tool that exposes the full GitHub surface — repos, PRs, issues, Actions runs, releases, gists, and the API — without opening a browser. It ships prebuilt for Windows/macOS/Linux and integrates tightly with git.
Authentication
$ gh auth login
$ gh auth login --hostname github.mycompany.com
$ echo "$GH_TOKEN" | gh auth login --with-token
$ gh auth status
github.com
✓ Logged in to github.com as octocat (~/.config/gh/hosts.yml)
✓ Git operations for github.com configured to use https protocol
✓ Token: ghp_***
For automated environments set the GH_TOKEN environment variable to a PAT or to GITHUB_TOKEN from an Actions workflow — gh picks it up automatically without running auth login.
Config
$ gh config list
$ gh config set git_protocol ssh
$ gh config set editor "code --wait"
$ gh config set browser firefox
$ gh config set --host github.mycompany.com git_protocol ssh
Extensions
Extensions are standalone executables prefixed gh-. They extend gh with community-built subcommands and can be written in any language. The GitHub team and community publish dozens of extensions on GitHub Marketplace.
$ gh extension search copilot
$ gh extension install github/gh-copilot
$ gh extension list
$ gh extension upgrade --all
$ gh extension remove github/gh-copilot
Aliases
Aliases let you bind a short name to any gh invocation — including multi-flag commands you run constantly.
$ gh alias set prm 'pr merge --squash --auto --delete-branch'
$ gh prm
$ gh alias set mypr 'pr create --title "$1" --body "" --draft'
$ gh mypr "WIP: my feature"
$ gh alias set clone-and-cd '!gh repo clone "$1" && cd "$(basename $1)"'
$ gh alias list
$ gh alias delete prm
PRO TIP
Store aliases in dotfiles and commit them. Run gh alias import <file> and gh alias export to share them across machines. Aliases live in ~/.config/gh/config.yml.
12.2
gh for Daily Work: PRs, Issues & Repos
Once you know the verbs — create, list, view, edit, merge, close — you can compose them into fast terminal workflows that keep your hands off the browser.
Pull Request workflow
$ gh pr create
$ gh pr create \
--title "feat: add dark mode toggle" \
--body "$(cat .github/pull_request_template.md)" \
--assignee @me \
--reviewer octocat,team:platform \
--label enhancement \
--draft
$ gh pr list
$ gh pr checkout 217
$ gh pr view 217 --web
$ gh pr view 217
$ gh pr review 217 --approve --body "LGTM, shipping it"
$ gh pr review 217 --request-changes --body "See inline comments"
$ gh pr merge 217 --squash --delete-branch
$ gh pr merge 217 --rebase
$ gh pr merge 217 --merge
$ gh pr merge 217 --squash --auto
Issue workflow
$ gh issue create \
--title "Button click triggers page reload on Safari" \
--body "Steps to reproduce: ..." \
--label "bug,priority:high" \
--assignee octocat \
--milestone "v2.4"
$ gh issue list --label "priority:high" --state open
$ gh issue list --assignee @me --limit 20
$ gh issue close 88 --comment "Fixed in #217. Will ship in v2.4."
$ gh issue pin 88
$ gh issue lock 88 --reason resolved
Repo & gist workflow
$ gh repo clone acme/platform
$ gh repo fork acme/platform --clone
$ gh repo create acme/new-service --private --source=. --push
$ gh repo view acme/platform
$ gh repo sync acme/platform --branch main
$ gh gist create snippet.py --description "rate limiter helper" --public
Actions workflow commands
$ gh workflow run deploy.yml --ref main --field env=prod
$ gh run watch
$ gh run list --workflow=ci.yml --limit 5
$ gh run download 9876543 --name coverage-report
12.3
gh Scripting: --json, --jq & Shell Pipelines
Every gh list and view command accepts --json to output structured data and --jq to filter it inline. Together they turn gh into a composable building block for shell scripts, cron jobs, and CI automation.
The --json / --jq pattern
$ gh pr list --json
error: the `--json` flag requires one or more comma separated field names
Supported fields: number, title, author, state, labels, createdAt, mergedAt, ...
$ gh pr list --json number,title,author
[{"number":217,"title":"feat: dark mode","author":{"login":"octocat"}}, ...]
$ gh pr list --json number,title,author \
--jq '.[] | "\(.number)\t\(.author.login)\t\(.title)"'
217 octocat feat: dark mode
214 alice fix: button reload on Safari
$ gh pr list --json number --jq '.[0].number'
217
Useful scripting patterns
$ gh pr list --author octocat --state open \
--json number,title,createdAt \
--jq '.[] | select(.createdAt > "2026-01-01")'
$ gh issue list --label bug --state open --limit 200 \
--json number --jq 'length'
$ gh repo list acme --limit 200 --json name \
--jq '.[].name' > repos.txt
$ while IFS= read -r repo; do
count=$(gh issue list -R "acme/$repo" --label bug --state open \
--json number --jq 'length' 2>/dev/null)
echo "$repo: $count open bugs"
done < repos.txt
$ gh api repos/acme/platform/branches/main --jq '.commit.sha'
Template output with --template
$ gh pr list --template \
'{{range .}}{{tablerow .number .title .author.login .createdAt}}{{end}}'
SCRIPTING RULE
Always use --json + --jq over parsing human-readable output with grep/awk. The JSON schema is stable; the human-readable format changes between versions and locale settings.
12.4
REST API v3: Authentication, Rate Limits & Pagination
GitHub's REST API (v3) is a JSON-over-HTTPS API covering almost every GitHub resource. gh api is the easiest way to call it without managing auth headers manually.
Calling the REST API with gh api
$ gh api repos/acme/platform
$ gh api repos/acme/platform/issues \
--method POST \
--field title="Automated bug report" \
--field body="Found by nightly scan" \
--field 'labels[]=bug'
$ gh api repos/acme/platform/issues/88 \
--method PATCH \
--field state=closed
$ gh api repos/acme/platform/git/refs/heads/old-feature \
--method DELETE
Calling the REST API directly (curl / fetch)
$ curl -H "Authorization: Bearer $GH_TOKEN" \
-H "Accept: application/vnd.github+json" \
-H "X-GitHub-Api-Version: 2022-11-28" \
https://api.github.com/repos/acme/platform
$ curl -H "Authorization: Bearer $INSTALLATION_TOKEN" \
https://api.github.com/repos/acme/platform/issues
Rate limits
| Auth type | Primary limit | Secondary limit |
| Unauthenticated | 60 req/hour per IP | N/A |
| PAT / OAuth App | 5,000 req/hour per user | 100 concurrent, no burst |
| GitHub App (installation) | 5,000 req/hour per installation + 50/hour per core for GitHub Enterprise | Same secondary rules |
| GITHUB_TOKEN (Actions) | 1,000 req/hour per repo per workflow | Same secondary rules |
$ gh api rate_limit --jq '.rate'
{"limit":5000,"used":47,"remaining":4953,"reset":1749024000}
Pagination
The REST API returns at most 100 items per page. Use the Link response header to traverse pages, or let gh api --paginate do it for you.
$ gh api repos/acme/platform/issues?state=open&per_page=100 \
--paginate --jq '.[].number'
$ page=1
$ while :; do
body=$(curl -s -H "Authorization: Bearer $GH_TOKEN" \
"https://api.github.com/repos/acme/platform/issues?state=open&per_page=100&page=$page")
echo "$body" | jq -e 'length > 0' >/dev/null || break
echo "$body" | jq '.[].number'
((page++))
done
Common endpoints quick reference
| Method | Endpoint | Purpose |
| GET | /repos/{owner}/{repo} | Repo metadata |
| GET | /repos/{owner}/{repo}/pulls?state=open | List PRs |
| POST | /repos/{owner}/{repo}/issues | Create issue |
| PATCH | /repos/{owner}/{repo}/issues/{n} | Update issue (close, label) |
| POST | /repos/{owner}/{repo}/issues/{n}/comments | Add comment |
| GET | /orgs/{org}/members | List org members |
| GET | /repos/{owner}/{repo}/actions/runs | List workflow runs |
| POST | /repos/{owner}/{repo}/releases | Create release |
12.5
GraphQL API v4: Queries, Cost & Rate Limits
The GraphQL API exists because the REST API over-fetches. A REST call to list 100 issues returns every field on every issue — but you might only need title and number. GraphQL lets you declare exactly what you want, reducing payload size and enabling complex nested queries in a single request.
Why GraphQL for GitHub?
| Scenario | REST | GraphQL |
| Fetch issue + PR + project status in one call | 3 separate requests | 1 query |
| Fetch only title+number from 100 issues | Full issue object every time | Exactly 2 fields |
| Query Projects v2 custom fields | Not supported | Supported |
| Mutations (update project item, add label) | PATCH requests | Typed mutations with input validation |
Writing queries with gh api graphql
$ gh api graphql -f query='
query {
repository(owner:"acme", name:"platform") {
pullRequests(last:5, states:OPEN) {
nodes {
number
title
author { login }
createdAt
}
}
}
}
'
$ gh api graphql \
-F owner=acme \
-F repo=platform \
-F last=10 \
-f query='
query($owner:String!, $repo:String!, $last:Int!) {
repository(owner:$owner, name:$repo) {
pullRequests(last:$last, states:OPEN) {
nodes { number title author { login } }
}
}
}
' --jq '.data.repository.pullRequests.nodes'
Cursor-based pagination
$ gh api graphql -f query='
query {
repository(owner:"acme", name:"platform") {
issues(first:100, states:OPEN, after:null) {
pageInfo { hasNextPage endCursor }
nodes { number title }
}
}
}
'
$ gh api graphql --paginate -f query='
query($endCursor:String) {
repository(owner:"acme", name:"platform") {
issues(first:100, after:$endCursor, states:OPEN) {
pageInfo { hasNextPage endCursor }
nodes { number title }
}
}
}
' --jq '.data.repository.issues.nodes[].number'
Mutations
$ gh api graphql \
-F issueId="I_kwDOABcdef01A" \
-F labelId="LA_kwDOABcdef02B" \
-f query='
mutation($issueId:ID!, $labelId:ID!) {
addLabelsToLabelable(input:{labelableId:$issueId, labelIds:[$labelId]}) {
labelable { ... on Issue { number title } }
}
}
'
Cost & rate limits
GraphQL uses a node cost model instead of a request count. Each node (object) fetched costs 1 point. You get 5,000 points per hour for authenticated requests. Complex nested queries can cost hundreds of points.
$ gh api graphql -f query='
query {
rateLimit {
cost remaining resetAt
}
repository(owner:"acme", name:"platform") {
issues(first:10) { nodes { number } }
}
}
' --jq '.data.rateLimit'
{"cost": 1, "remaining": 4987, "resetAt": "2026-06-14T10:00:00Z"}
RATE LIMIT GOTCHA
A query that fetches 100 issues × 10 nested PRs each = up to 1,000 nodes = 1,000 points in a single call. Always check rateLimit.cost in development. Set first: to the minimum you actually need.
12.6
Octokit SDK: Setup, Pagination Helpers & Throttling
Octokit is the official GitHub SDK family. It handles auth, retries, pagination, and rate-limit throttling so you don't have to re-implement those in every script. The JavaScript/TypeScript client (@octokit/rest) is the most mature; Python (PyGithub) and Ruby (octokit.rb) are maintained but slightly less feature-rich.
JavaScript / TypeScript setup
$ npm install @octokit/rest @octokit/plugin-throttling @octokit/plugin-retry
import { Octokit } from "@octokit/rest";
import { throttling } from "@octokit/plugin-throttling";
import { retry } from "@octokit/plugin-retry";
const MyOctokit = Octokit.plugin(throttling, retry);
const octokit = new MyOctokit({
auth: process.env.GH_TOKEN,
throttle: {
onRateLimit: (retryAfter, options, _octokit, retryCount) => {
console.warn(`Rate limited. Retry after ${retryAfter}s`);
return retryCount < 3;
},
onSecondaryRateLimit: (retryAfter, options) => {
console.warn(`Secondary limit hit for ${options.url}`);
return true;
},
},
retry: { doNotRetry: ["429"] },
});
Calling REST endpoints
const issues = await octokit.paginate(octokit.rest.issues.listForRepo, {
owner: "acme",
repo: "platform",
state: "open",
per_page: 100,
});
console.log(`${issues.length} open issues`);
const { data: newIssue } = await octokit.rest.issues.create({
owner: "acme",
repo: "platform",
title: "Automated: dependency drift detected",
body: "See nightly scan report attached.",
labels: ["dependency-update"],
});
console.log(`Created #${newIssue.number}`);
GraphQL with Octokit
const { repository } = await octokit.graphql(`
query($owner:String!, $repo:String!) {
repository(owner:$owner, name:$repo) {
pullRequests(first:50, states:OPEN) {
nodes { number title createdAt }
}
}
}
`, { owner: "acme", repo: "platform" });
const prs = octokit.graphql.paginate(`
query($owner:String!, $repo:String!, $cursor:String) {
repository(owner:$owner, name:$repo) {
pullRequests(first:100, states:OPEN, after:$cursor) {
pageInfo { hasNextPage endCursor }
nodes { number title }
}
}
}
`, { owner: "acme", repo: "platform" });
Python (PyGithub)
$ pip install PyGithub
from github import Github
g = Github(os.environ["GH_TOKEN"])
repo = g.get_repo("acme/platform")
open_issues = repo.get_issues(state="open")
for issue in open_issues:
if "bug" in [l.name for l in issue.labels]:
issue.create_comment("Triaged by automation")
12.7
GitHub Apps: Private Key Auth, Installation Tokens & Webhook Events
A GitHub App is a first-class principal on GitHub — distinct from a user account. Apps can be installed on specific repos with fine-grained permissions, reducing the blast radius compared to a full-access PAT. For automation that needs to act across many repos or that you want to ship to others, GitHub Apps are the right model.
PAT vs OAuth App vs GitHub App
| Type | Acts as | Permissions | Rate limit | Best for |
| Classic PAT | A user | Coarse scopes (repo, admin, etc.) | 5,000/hr per user | Personal scripts, quick access |
| Fine-grained PAT | A user | Per-repo, per-permission | 5,000/hr per user | Least-privilege personal scripts |
| OAuth App | A user (on behalf of) | Coarse scopes, user consent | 5,000/hr per user | User-facing web apps |
| GitHub App | Itself (installation) | Fine-grained, per repo, per permission | 5,000/hr per installation | Bots, CI integrations, multi-repo tools |
Creating a GitHub App
- Go to Settings → Developer settings → GitHub Apps → New GitHub App
- Set Homepage URL, Webhook URL (or use smee.io for local dev), and Webhook Secret
- Under Permissions, select only what you need (e.g. Issues: Read/Write, Pull requests: Read/Write)
- Under Subscribe to events, select relevant webhook events
- Generate a private key (.pem file) — store it securely (never commit it)
- Install the app on your org or repo — this creates an installation
Authentication flow: JWT → installation token
import { createAppAuth } from "@octokit/auth-app";
import { Octokit } from "@octokit/rest";
import { readFileSync } from "fs";
const octokit = new Octokit({
authStrategy: createAppAuth,
auth: {
appId: process.env.GITHUB_APP_ID,
privateKey: readFileSync(process.env.GITHUB_APP_KEY_PATH, "utf8"),
installationId: process.env.GITHUB_INSTALLATION_ID,
},
});
const { data } = await octokit.rest.repos.listForInstallation({ per_page: 100 });
console.log(`App has access to ${data.length} repos`);
$ gh api /app/installations --jq '.[] | select(.account.login=="acme") | .id'
TOKEN LIFECYCLE
Installation access tokens expire after 1 hour. The @octokit/auth-app plugin caches them and transparently mints a new token before expiry. If you're rolling your own auth, check the expires_at field in the token response and refresh before it lapses.
12.8
Webhooks: Event Types, HMAC Verification & smee.io for Local Dev
Webhooks are HTTP POST callbacks that GitHub sends to your server when events happen in a repo or org. They are the backbone of any real-time GitHub automation — bots, CI triggers, audit pipelines, and Slack notifiers all start with a webhook.
Key event types
pull_request
Opened, closed, merged, converted to draft, labeled, review requested, synchronize (new commit pushed). The most-used event for CI bots and review automation.
push
Any push to any branch (not tags by default). Payload includes commits, before/after SHAs, and the pusher's identity.
issues
Opened, closed, assigned, labeled, milestoned, transferred, deleted. Used for triage bots and project board automation.
issue_comment
Created, edited, deleted on an issue or PR. Used for slash-command bots (e.g. /rebase triggered from a comment).
workflow_run
A GitHub Actions workflow completed. Used to trigger post-CI automation: deploy on green, notify on failure.
release
Created, published, edited, deleted. Used to trigger external deployment pipelines, changelog publishes, and Slack announcements.
membership / organization
Member added/removed from org or team. Used for onboarding automation and audit logging.
HMAC-SHA256 payload verification
Always verify the X-Hub-Signature-256 header. Without it, any HTTP client can POST a fake event to your webhook endpoint.
import { createHmac, timingSafeEqual } from "crypto";
app.post("/webhook", express.raw({ type: "application/json" }), (req, res) => {
const sig = req.headers["x-hub-signature-256"];
if (!sig) return res.status(403).send("No signature");
const expected = "sha256=" + createHmac("sha256", process.env.WEBHOOK_SECRET)
.update(req.body)
.digest("hex");
if (!timingSafeEqual(Buffer.from(sig), Buffer.from(expected))) {
return res.status(403).send("Signature mismatch");
}
const event = req.headers["x-github-event"];
const payload = JSON.parse(req.body);
if (event === "pull_request" && payload.action === "opened") {
handlePrOpened(payload);
}
res.status(200).send("OK");
});
SECURITY REQUIREMENT
Never use string equality (===) for HMAC comparison — it's vulnerable to timing attacks. Always use crypto.timingSafeEqual() (Node) or hmac.compare_digest() (Python). Webhook secrets should be randomly generated (32+ bytes).
smee.io for local development
GitHub can't send webhooks to localhost. smee.io is a hosted event relay that forwards GitHub webhook payloads to your local machine over EventSource (SSE).
$ npm install -g smee-client
$ smee --url https://smee.io/your-channel-id \
--target http://localhost:3000/webhook
Inspecting webhook delivery logs
GitHub logs every webhook delivery attempt. Go to Repo Settings → Webhooks → Recent Deliveries to see the payload, response, and redeliver if needed. For GitHub Apps, check App Settings → Advanced → Recent Deliveries.
12.9
Practical Automations: Auto-Assign, Stale Closer, Release Notifier & Org Onboarding Bot
These four patterns cover the most common automation needs for a senior dev or lead. Each can be implemented either as a GitHub Actions workflow (simpler, no infrastructure) or as a webhook handler (more control, external trigger).
1. Auto-Assign Reviewers
Trigger: pull_request.opened | Implementation: Actions workflow + CODEOWNERS
CODEOWNERS already handles reviewer requests — but you sometimes need custom logic (round-robin, on-call rotation, skill-based routing). The auto-assign action covers the common case; a custom workflow script covers the rest.
on:
pull_request:
types: [opened, ready_for_review]
jobs:
assign:
runs-on: ubuntu-latest
if: "!github.event.pull_request.draft"
steps:
- uses: actions/checkout@v4
- name: Assign reviewers
uses: hmarr/auto-approve-action@v4
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
2. Stale Issue Closer
Trigger: schedule (daily) | Implementation: actions/stale or custom script
Close issues and PRs that have had no activity after a set period. The official actions/stale action handles the most common case with a label-then-close grace period.
on:
schedule:
- cron: "0 2 * * *"
workflow_dispatch:
jobs:
stale:
runs-on: ubuntu-latest
steps:
- uses: actions/stale@v9
with:
stale-issue-message: "This issue has been inactive for 60 days. It will be closed in 7 days unless there is new activity."
stale-pr-message: "This PR has been inactive for 30 days. It will be closed in 7 days."
days-before-issue-stale: 60
days-before-pr-stale: 30
days-before-issue-close: 7
days-before-pr-close: 7
stale-issue-label: "stale"
exempt-issue-labels: "pinned,security,roadmap"
exempt-pr-labels: "do-not-close"
3. Release Notifier
Trigger: release.published | Implementation: Actions workflow
Post a Slack or Teams message when a new GitHub Release is published. Link to the release notes, include the tag, and optionally diff the changelog from the last release.
on:
release:
types: [published]
jobs:
notify:
runs-on: ubuntu-latest
steps:
- name: Post to Slack
uses: slackapi/slack-github-action@v1
with:
payload: |
{
"text": "🚀 *${{ github.event.repository.name }} ${{ github.event.release.tag_name }} released!*\n${{ github.event.release.html_url }}"
}
env:
SLACK_WEBHOOK_URL: ${{ secrets.SLACK_RELEASE_WEBHOOK }}
SLACK_WEBHOOK_TYPE: INCOMING_WEBHOOK
4. Org Onboarding Bot
Trigger: organization.member_added webhook | Implementation: webhook handler (Node.js + Octokit)
When a new member joins the org: add them to a standard team, create a welcome issue in the onboarding repo, and post a welcome message. This is best implemented as a GitHub App webhook handler since it needs org-level scope.
async function handleMemberAdded(payload) {
const { login } = payload.membership.user;
const org = payload.organization.login;
await octokit.rest.teams.addOrUpdateMembershipForUserInOrg({
org, team_slug: "all-engineers", username: login,
});
await octokit.rest.issues.create({
owner: org, repo: "onboarding",
title: `Welcome @${login}!`,
body: `Hi @${login} — here's your onboarding checklist:\n- [ ] Set up 2FA\n- [ ] Read the team handbook\n- [ ] Join #engineering on Slack`,
assignees: [login],
labels: ["onboarding"],
});
await postSlack(`👋 @${login} just joined ${org}! Say hi in #introductions.`);
}
12.10
GitHub Actions + GraphQL: Querying Projects v2 & Updating Custom Fields from Workflows
GitHub Projects v2 has no REST API. All programmatic access goes through the GraphQL API. This section shows how to wire GraphQL calls into Actions workflows so your CI can read and update project state: set a status field on merge, move items when a release ships, or query project data for reporting.
Required permissions
permissions:
issues: write
pull-requests: write
repository-projects: write
IMPORTANT
GITHUB_TOKEN cannot access org-level Projects. For org projects you must create a PAT (classic, with project scope) or a GitHub App installation token and pass it as a secret.
Finding your project ID and field IDs
$ gh api graphql -F org=acme -f query='
query($org:String!) {
organization(login:$org) {
projectsV2(first:20) {
nodes { id number title }
}
}
}
' --jq '.data.organization.projectsV2.nodes'
$ gh api graphql -F projectId="PVT_kwDOABcd01A" -f query='
query($projectId:ID!) {
node(id:$projectId) {
... on ProjectV2 {
fields(first:20) {
nodes {
... on ProjectV2Field { id name }
... on ProjectV2SingleSelectField { id name options { id name } }
... on ProjectV2IterationField { id name }
}
}
}
}
}
'
Complete workflow: update Status field on PR merge
name: Update project on merge
on:
pull_request:
types: [closed]
jobs:
update-project:
runs-on: ubuntu-latest
if: github.event.pull_request.merged == true
steps:
- name: Move PR item to Done in project
env:
GH_TOKEN: ${{ secrets.PROJECT_PAT }}
PR_ID: ${{ github.event.pull_request.node_id }}
PROJECT_ID: PVT_kwDOABcd01A
STATUS_FIELD_ID: PVTF_lADOABcd01A
DONE_OPTION_ID: done_option_node_id
run: |
item_id=$(gh api graphql \
-F prId="$PR_ID" -F projectId="$PROJECT_ID" \
-f query='
query($prId:ID!, $projectId:ID!) {
node(id:$prId) {
... on PullRequest {
projectItems(first:10) {
nodes { id project { id } }
}
}
}
}
' \
--jq ".data.node.projectItems.nodes[] | select(.project.id==\"$PROJECT_ID\") | .id")
gh api graphql \
-F itemId="$item_id" -F projectId="$PROJECT_ID" \
-F fieldId="$STATUS_FIELD_ID" -F optionId="$DONE_OPTION_ID" \
-f query='
mutation($projectId:ID!, $itemId:ID!, $fieldId:ID!, $optionId:String!) {
updateProjectV2ItemFieldValue(input:{
projectId:$projectId itemId:$itemId fieldId:$fieldId
value:{ singleSelectOptionId:$optionId }
}) {
projectV2Item { id }
}
}
'
echo "Status updated to Done"
Sprint report: count items by status
$ gh api graphql -F projectId="PVT_kwDOABcd01A" -f query='
query($projectId:ID!) {
node(id:$projectId) {
... on ProjectV2 {
items(first:200) {
nodes {
fieldValueByName(name:"Status") {
... on ProjectV2ItemFieldSingleSelectValue { name }
}
}
}
}
}
}
' --jq '[.data.node.items.nodes[].fieldValueByName.name] | group_by(.) | map({status:.[0],count:length})'
[
{"status":"Done","count":28},
{"status":"In Progress","count":7},
{"status":"In Review","count":4},
{"status":"Todo","count":15}
]
PATTERN
Store project IDs and field IDs in repository variables (not secrets) so workflows read them with vars.PROJECT_ID. Update the variable when you rename a field without touching the workflow file.
Up Next — Phase 13: GitHub Copilot & AI-Assisted Development
Copilot tiers, Chat slash commands, PR description generation, Copilot Workspace, content exclusions, measuring ROI, and prompt engineering for better completions.
Continue →
Back to Hub