Github Operations
GitHub CLI operations for issues, PRs, milestones, and Projects v2. Covers gh commands, REST API patterns, and automation scripts. Use when managing GitHub issues, PRs, milestones, or Projects with gh.
GitHub Operations
Comprehensive GitHub CLI (gh) operations for project management, from basic issue creation to advanced Projects v2 integration and milestone tracking via REST API.
Overview
- Creating and managing GitHub issues and PRs
- Working with GitHub Projects v2 custom fields
- Managing milestones (sprints, releases) via REST API
- Automating bulk operations with
gh - Running GraphQL queries for complex operations
Quick Reference
Issue Operations
# Create issue with labels and milestone
gh issue create --title "Bug: API returns 500" --body "..." --label "bug" --milestone "Sprint 5"
# List and filter issues
gh issue list --state open --label "backend" --assignee @me
# Edit issue metadata
gh issue edit 123 --add-label "high" --milestone "v2.0"PR Operations
# Create PR with reviewers
gh pr create --title "feat: Add search" --body "..." --base dev --reviewer @teammate
# Watch CI status and auto-merge
gh pr checks 456 --watch
gh pr merge 456 --auto --squash --delete-branch
# Resume a session linked to a PR (CC 2.1.27)
claude --from-pr 456 # Resume session with PR context (diff, comments, review status)
claude --from-pr https://github.com/org/repo/pull/456Tip (CC 2.1.27): Sessions created via
gh pr createare automatically linked to the PR. Use--from-prto resume with full PR context.
Milestone Operations (REST API)
Footgun:
gh issue edit --milestonetakes a NAME (string), not a number. The REST API uses a NUMBER (integer). Never pass a number to--milestone. See CLI-vs-API Identifiers.
# List milestones with progress
gh api repos/:owner/:repo/milestones --jq '.[] | "\(.title): \(.closed_issues)/\(.open_issues + .closed_issues)"'
# Create milestone with due date
gh api -X POST repos/:owner/:repo/milestones \
-f title="Sprint 8" -f due_on="2026-02-15T00:00:00Z"
# Close milestone (API uses number, not name)
MILESTONE_NUM=$(gh api repos/:owner/:repo/milestones --jq '.[] | select(.title=="Sprint 8") | .number')
gh api -X PATCH repos/:owner/:repo/milestones/$MILESTONE_NUM -f state=closed
# Assign issues to milestone (CLI uses name, not number)
gh issue edit 123 124 125 --milestone "Sprint 8"Projects v2 Operations
# Add issue to project
gh project item-add 1 --owner @me --url https://github.com/org/repo/issues/123
# Set custom field (requires GraphQL)
gh api graphql -f query='mutation {...}' -f projectId="..." -f itemId="..."JSON Output Patterns
# Get issue numbers matching criteria
gh issue list --json number,labels --jq '[.[] | select(.labels[].name == "bug")] | .[].number'
# PR summary with author
gh pr list --json number,title,author --jq '.[] | "\(.number): \(.title) by \(.author.login)"'
# Find ready-to-merge PRs
gh pr list --json number,reviewDecision,statusCheckRollupState \
--jq '[.[] | select(.reviewDecision == "APPROVED" and .statusCheckRollupState == "SUCCESS")]'Key Concepts
Milestone vs Epic
| Milestones | Epics |
|---|---|
| Time-based (sprints, releases) | Topic-based (features) |
| Has due date | No due date |
| Progress bar | Task list checkbox |
| Native REST API | Needs workarounds |
Rule: Use milestones for "when", use parent issues for "what".
Projects v2 Custom Fields
Projects v2 uses GraphQL for setting custom fields (Status, Priority, Domain). Basic gh project commands work for listing and adding items, but field updates require GraphQL mutations.
Rules Quick Reference
| Rule | Impact | What It Covers |
|---|---|---|
| issue-tracking-automation | HIGH | Auto-progress from commits, sub-task completion, session summaries |
| issue-branch-linking | MEDIUM | Branch naming, commit references, PR linking patterns |
Batch Issue Creation
When creating multiple issues at once (e.g., seeding a sprint), use an array-driven loop:
# Define issues as an array of "title|labels|milestone" entries
SPRINT="Sprint 9"
ISSUES=(
"feat: Add user auth|enhancement,backend|$SPRINT"
"fix: Login redirect loop|bug,high|$SPRINT"
"chore: Update dependencies|maintenance|$SPRINT"
)
for entry in "${ISSUES[@]}"; do
IFS='|' read -r title labels milestone <<< "$entry"
NUM=$(gh issue create \
--title "$title" \
--label "$labels" \
--milestone "$milestone" \
--body "" \
--json number --jq '.number')
echo "Created #$NUM: $title"
doneTip: Capture the created issue number with
--json number --jq '.number'so you can reference it immediately (e.g., add to Projects v2, link in PRs).
Best Practices
- Always use
--jsonfor scripting - Parse with--jqfor reliability - Non-interactive mode for automation - Use
--title,--bodyflags - Check rate limits before bulk operations -
gh api rate_limit - Use heredocs for multi-line content -
--body "$(cat <<'EOF'...EOF)" - Link issues in PRs -
Closes #123,Fixes #456— GitHub auto-closes on merge - Use ISO 8601 dates -
YYYY-MM-DDTHH:MM:SSZfor milestone due_on - Close milestones, don't delete - Preserve history
--milestonetakes NAME, not number - See CLI-vs-API Identifiers- Never
gh issue closedirectly - Comment progress withgh issue comment; issues close only when their linked PR merges to the default branch
Related Skills
ork:create-pr- Create pull requests with proper formatting and review assignmentsork:review-pr- Comprehensive PR review with specialized agentsork:release-management- GitHub release workflow with semantic versioning and changelogsstacked-prs- Manage dependent PRs with rebase coordinationork:issue-progress-tracking- Automatic issue progress updates from commits
Key Decisions
| Decision | Choice | Rationale |
|---|---|---|
| CLI vs API | gh CLI preferred | Simpler auth, better UX, handles pagination automatically |
| Output format | --json with --jq | Reliable parsing for automation, no regex parsing needed |
| Milestones vs Epics | Milestones for time | Milestones have due dates and progress bars, epics for topic grouping |
| Projects v2 fields | GraphQL mutations | gh project commands limited, GraphQL required for custom fields |
| Milestone lifecycle | Close, don't delete | Preserves history and progress tracking |
References
- Issue Management - Bulk operations, templates, sub-issues
- PR Workflows - Reviews, merge strategies, auto-merge
- Milestone API - REST API patterns for milestone CRUD
- Projects v2 - Custom fields, GraphQL mutations
- GraphQL API - Complex queries, pagination, bulk operations
- CLI vs API Identifiers - NAME vs NUMBER footguns, milestone/project ID mapping
Examples
- Automation Scripts - Ready-to-use scripts for bulk operations, PR automation, milestone management
Rules (2)
Linking Issues to Branches and PRs — MEDIUM
Linking Issues to Branches and PRs
Establish bidirectional links between issues, branches, commits, and PRs for full traceability and automatic issue closure.
Branch Naming Convention
# Pattern: {type}/{issue-number}-{description}
issue/123-implement-feature
fix/456-resolve-timeout-bug
feature/789-add-search-api
# Creates automatic link: branch -> issueCommit Linking
# Reference in commit message
git commit -m "feat(#123): Add user validation"
# Auto-close keywords (in commit or PR body)
git commit -m "fix: Resolve timeout (closes #456)"
git commit -m "feat: Add search (fixes #789)"PR Linking
# Create PR that auto-closes issue on merge
gh pr create --title "feat(#123): Add search" \
--body "Closes #123
## Changes
- Added search API endpoint
- Added search UI component"
# Link existing PR to issue
gh issue edit 123 --add-label "has-pr"PR-Aware Session Resumption
# Resume with full PR context (CC 2.1.27+)
claude --from-pr 42 # Loads PR diff, comments, review statusLinking Checklist
| Link | How | Automatic? |
|---|---|---|
| Branch to issue | Branch name issue/N-* | Yes (hooks) |
| Commit to issue | #N in commit message | Yes (GitHub) |
| PR to issue | Closes #N in PR body | Yes (GitHub) |
| Issue to PR | has-pr label | Manual or hook |
Incorrect — Branch without issue number prefix:
git checkout -b implement-feature
git commit -m "Add user validation"
# No automatic linking - reviewer lacks contextCorrect — Issue-prefixed branch with linked commit:
git checkout -b issue/123-implement-feature
git commit -m "feat(#123): Add user validation"
# Auto-links: branch → issue, commit → issueKey Rules
- Always start branches with issue number prefix for automatic detection
- Use
Closes #Nin PR body for automatic issue closure on merge - Include
#Nin every commit that relates to an issue - Use conventional commit format for consistent linking
- Add
has-prlabel to issues when a PR is created - Use
--from-prto resume sessions with full PR context
Automate issue progress updates so stakeholders always see current status — HIGH
Automated Issue Progress Updates
Track issue progress automatically through commit detection, sub-task matching, and session summaries. Eliminates manual status updates.
Three-Hook Pipeline
| Hook | Trigger | Action |
|---|---|---|
| Commit Detection | Each commit | Extracts issue number, queues for batch comment |
| Sub-task Updater | Commit message match | Checks off matching - [ ] items in issue body |
| Session Summary | Session end | Posts consolidated progress comment |
Issue Number Extraction
# From branch name (priority)
issue/123-implement-feature # Extracts: 123
fix/456-resolve-bug # Extracts: 456
feature/789-add-tests # Extracts: 789
# From commit message (fallback)
"feat(#123): Add user validation" # Extracts: 123
"fix: Resolve bug (closes #456)" # Extracts: 456Sub-task Auto-Completion
Commit messages are matched against issue checkboxes using normalized text comparison:
# Issue body (before)
- [ ] Add input validation
- [ ] Write unit tests
# Commit: "feat(#123): Add input validation"
# Issue body (after)
- [x] Add input validation
- [ ] Write unit testsSession Summary Format
## Claude Code Progress Update
**Session**: `abc12345...`
**Branch**: `issue/123-implement-feature`
### Commits (3)
- `abc1234`: feat(#123): Add input validation
- `def5678`: test(#123): Add unit tests
### Files Changed
- `src/validation.ts` (+45, -12)
- `tests/validation.test.ts` (+89, -0)
### Sub-tasks Completed
- [x] Add input validation
- [x] Write unit testsIncorrect — Manual issue updates without automation:
# Commit without issue reference
git commit -m "Add validation"
# Manually comment on issue #123:
"Added validation - see commit abc1234"
[Time-consuming, error-prone]Correct — Automated progress tracking:
# Issue-prefixed branch
git checkout -b issue/123-validation
# Conventional commit
git commit -m "feat(#123): Add input validation"
# Hook auto-posts to issue:
"[Session abc123] feat(#123): Add input validation
Files: src/validation.ts (+45)"Key Rules
- Use issue-prefixed branches (
issue/N-,fix/N-,feature/N-) for automatic detection - Include
#Nin commit messages as fallback for issue linking - Use conventional commits (
feat(#123):,fix(#123):) for reliable matching - Match commit message text to checkbox descriptions for auto-completion
- Post consolidated summaries at session end, not per-commit
References (6)
Cli Vs Api Identifiers
CLI vs REST API Identifier Mapping
GitHub CLI (gh) and the REST API use different identifier types for the same resources. Mixing them is the #1 source of silent failures in automation.
Quick Reference
| Resource | gh CLI flag | REST API field | Example |
|---|---|---|---|
| Milestone | --milestone "Sprint 8" (NAME) | milestones/:number (INTEGER) | CLI: "Sprint 8" → API: /milestones/5 |
| Issue | gh issue view 123 (number) | issues/:number (INTEGER) | Same — issue number works in both |
| PR | gh pr view 456 (number) | pulls/:number (INTEGER) | Same — PR number works in both |
| User | --assignee "username" (LOGIN) | assignees/:username (STRING) | Same format, no confusion |
| Label | --label "bug" (NAME) | labels by name only | Same — no number in API either |
| Project | gh project uses NUMBER | Projects v2 uses node_id | Different! GraphQL needs node_id |
The Milestone Footgun
gh issue edit --milestone and gh issue list --milestone accept a milestone NAME (string), not a number.
The REST API endpoint is repos/:owner/:repo/milestones/:number — it uses the milestone NUMBER (integer).
# CORRECT: gh CLI uses milestone NAME
gh issue edit 123 --milestone "Sprint 8" # ✓ Name
gh issue list --milestone "Sprint 8" # ✓ Name
# CORRECT: REST API uses milestone NUMBER
gh api -X PATCH repos/:owner/:repo/milestones/5 -f state=closed # ✓ Number
# WRONG: don't pass a number to --milestone
gh issue edit 123 --milestone 5 # ✗ Silently fails or wrong milestoneLook Up a Milestone Number
When you need a number (for REST API calls), look it up from the name:
# Get milestone number from name
MILESTONE_NUM=$(gh api repos/:owner/:repo/milestones \
--jq '.[] | select(.title == "Sprint 8") | .number')
# Then use the number for REST API calls
gh api -X PATCH repos/:owner/:repo/milestones/$MILESTONE_NUM -f state=closedProjects v2 Identifier Confusion
Projects v2 has an extra layer: the project number (shown in URL) vs the project node_id (needed for GraphQL mutations).
# List projects — shows both number and id
gh project list --owner @me --format json --jq '.projects[] | {number, id}'
# Output: {"number": 1, "id": "PVT_kwDOAbc123"}
# gh project commands use NUMBER
gh project item-add 1 --owner @me --url https://github.com/org/repo/issues/123
# GraphQL mutations need node_id (the "PVT_..." value)
gh api graphql -f query='
mutation {
updateProjectV2ItemFieldValue(input: {
projectId: "PVT_kwDOAbc123" # node_id, not number
...
})
}
'Issue and PR Numbers
Issues and PRs use the same number in both CLI and REST API — no translation needed.
# Both work identically
gh issue view 123
gh api repos/:owner/:repo/issues/123
# JSON output includes both number and node_id
gh issue view 123 --json number,id
# {"number": 123, "id": "I_kwDOAbc123"}GraphQL sub-issue mutations require the node_id (I_kwDO...), not the number.
Safe Patterns
Always resolve milestone names to numbers before REST API calls
get_milestone_number() {
local name="$1"
gh api repos/:owner/:repo/milestones \
--jq --arg name "$name" '.[] | select(.title == $name) | .number'
}
MS_NUM=$(get_milestone_number "Sprint 8")
gh api -X PATCH repos/:owner/:repo/milestones/$MS_NUM -f state=closedUse --json to capture the created resource's identifier
# Capture milestone number at creation time
MILESTONE_NUM=$(gh api -X POST repos/:owner/:repo/milestones \
-f title="Sprint 9" \
--jq '.number')
# Now you have the number for REST API calls
gh api repos/:owner/:repo/milestones/$MILESTONE_NUMGraphql Api
GraphQL API with gh
Basic GraphQL Query
gh api graphql -f query='
query {
viewer {
login
name
}
}
'Variables in Queries
gh api graphql \
-F owner="org" \
-F repo="repo-name" \
-f query='
query($owner: String!, $repo: String!) {
repository(owner: $owner, name: $repo) {
issues(first: 10, states: OPEN) {
nodes {
number
title
}
}
}
}
'Note: Use -F for non-string values (numbers, booleans), -f for strings.
Common Queries
Repository Info
gh api graphql -f query='
query($owner: String!, $repo: String!) {
repository(owner: $owner, name: $repo) {
name
description
stargazerCount
forkCount
issues(states: OPEN) { totalCount }
pullRequests(states: OPEN) { totalCount }
}
}
' -f owner="org" -f repo="repo-name"Issue with Labels and Milestone
gh api graphql -f query='
query($owner: String!, $repo: String!, $number: Int!) {
repository(owner: $owner, name: $repo) {
issue(number: $number) {
title
body
state
labels(first: 10) {
nodes { name color }
}
milestone {
title
dueOn
}
assignees(first: 5) {
nodes { login }
}
}
}
}
' -f owner="org" -f repo="repo-name" -F number=123PR with Reviews and Checks
gh api graphql -f query='
query($owner: String!, $repo: String!, $number: Int!) {
repository(owner: $owner, name: $repo) {
pullRequest(number: $number) {
title
reviewDecision
mergeable
commits(last: 1) {
nodes {
commit {
statusCheckRollup {
state
contexts(first: 10) {
nodes {
... on CheckRun {
name
conclusion
}
}
}
}
}
}
}
reviews(last: 10) {
nodes {
author { login }
state
submittedAt
}
}
}
}
}
' -f owner="org" -f repo="repo-name" -F number=456Pagination
# Use --paginate for automatic pagination
gh api graphql --paginate \
-F owner="org" \
-F repo="repo-name" \
-f query='
query($owner: String!, $repo: String!, $endCursor: String) {
repository(owner: $owner, name: $repo) {
issues(first: 100, after: $endCursor, states: OPEN) {
nodes {
number
title
}
pageInfo {
hasNextPage
endCursor
}
}
}
}
'Important: For pagination to work:
- Include
$endCursor: Stringin query variables - Include
pageInfo \{ hasNextPage endCursor \}in response - Use
after: $endCursorin the connection
Mutations
Add Label to Issue
# First get label ID
LABEL_ID=$(gh api graphql -f query='
query($owner: String!, $repo: String!, $name: String!) {
repository(owner: $owner, name: $repo) {
label(name: $name) { id }
}
}
' -f owner="org" -f repo="repo-name" -f name="bug" \
--jq '.data.repository.label.id')
# Get issue ID
ISSUE_ID=$(gh api graphql -f query='
query($owner: String!, $repo: String!, $number: Int!) {
repository(owner: $owner, name: $repo) {
issue(number: $number) { id }
}
}
' -f owner="org" -f repo="repo-name" -F number=123 \
--jq '.data.repository.issue.id')
# Add label
gh api graphql -f query='
mutation($issueId: ID!, $labelIds: [ID!]!) {
addLabelsToLabelable(input: {
labelableId: $issueId
labelIds: $labelIds
}) {
labelable {
... on Issue { title }
}
}
}
' -f issueId="$ISSUE_ID" -f labelIds="[\"$LABEL_ID\"]"Create Issue with GraphQL
gh api graphql -f query='
mutation($repoId: ID!, $title: String!, $body: String) {
createIssue(input: {
repositoryId: $repoId
title: $title
body: $body
}) {
issue {
number
url
}
}
}
' -f repoId="MDEwOlJlcG9zaXRvcnkxMjM0NTY3ODk=" \
-f title="New issue via GraphQL" \
-f body="Description here"Close Issue
gh api graphql -f query='
mutation($issueId: ID!) {
closeIssue(input: { issueId: $issueId }) {
issue {
state
closedAt
}
}
}
' -f issueId="I_kwDOABCD1234"JQ Processing
# Extract specific field
gh api graphql -f query='...' --jq '.data.repository.issues.nodes'
# Filter results
gh api graphql -f query='...' \
--jq '.data.repository.issues.nodes[] | select(.labels.nodes[].name == "bug")'
# Transform to custom format
gh api graphql -f query='...' \
--jq '.data.repository.issues.nodes[] | {num: .number, title: .title}'Error Handling
# Check for errors in response
RESULT=$(gh api graphql -f query='...')
if echo "$RESULT" | jq -e '.errors' > /dev/null 2>&1; then
echo "GraphQL Error:"
echo "$RESULT" | jq '.errors[].message'
exit 1
fi
# Process successful result
echo "$RESULT" | jq '.data'Useful Fragments
Reusable Issue Fragment
fragment IssueFields on Issue {
number
title
state
createdAt
updatedAt
labels(first: 10) {
nodes { name }
}
assignees(first: 5) {
nodes { login }
}
milestone {
title
}
}
query {
repository(owner: "org", name: "repo-name") {
issues(first: 10) {
nodes {
...IssueFields
}
}
}
}Bulk Operations
Update Multiple Issues
# Get all issues to update
ISSUES=$(gh api graphql -f query='
query {
repository(owner: "org", name: "repo-name") {
issues(first: 50, states: OPEN, labels: ["stale"]) {
nodes { id number }
}
}
}
' --jq '.data.repository.issues.nodes[]')
# Close each one
echo "$ISSUES" | while read -r issue; do
ISSUE_ID=$(echo "$issue" | jq -r '.id')
gh api graphql -f query='
mutation($id: ID!) {
closeIssue(input: { issueId: $id }) {
issue { number state }
}
}
' -f id="$ISSUE_ID"
doneRate Limit Checking
gh api graphql -f query='
query {
rateLimit {
limit
remaining
resetAt
used
}
}
'Get Node IDs
Many GraphQL mutations require node IDs (not numbers):
# Issue ID
gh api graphql -f query='
query($owner: String!, $repo: String!, $number: Int!) {
repository(owner: $owner, name: $repo) {
issue(number: $number) { id }
}
}
' -f owner="org" -f repo="repo-name" -F number=123 \
--jq '.data.repository.issue.id'
# Repository ID
gh api graphql -f query='
query($owner: String!, $repo: String!) {
repository(owner: $owner, name: $repo) { id }
}
' -f owner="org" -f repo="repo-name" \
--jq '.data.repository.id'
# Label ID
gh api graphql -f query='
query($owner: String!, $repo: String!, $name: String!) {
repository(owner: $owner, name: $repo) {
label(name: $name) { id }
}
}
' -f owner="org" -f repo="repo-name" -f name="bug" \
--jq '.data.repository.label.id'Issue Management
Issue Management
Creating Issues
Basic Creation
# Non-interactive (ideal for automation)
gh issue create \
--title "Bug: API returns 500 on invalid input" \
--body "Description of the issue..." \
--label "bug,backend" \
--assignee "@me" \
--milestone "Sprint 5"Note:
--milestonetakes the milestone NAME (string), not a number. See CLI vs API Identifiers for the full NAME/NUMBER mapping.
Batch Creation
Create multiple issues from an array — useful for seeding sprints or creating related tasks:
SPRINT="Sprint 9"
ISSUES=(
"feat: Add user auth|enhancement,backend"
"fix: Login redirect loop|bug,high"
"chore: Update CI dependencies|maintenance"
)
for entry in "${ISSUES[@]}"; do
IFS='|' read -r title labels <<< "$entry"
NUM=$(gh issue create \
--title "$title" \
--label "$labels" \
--milestone "$SPRINT" \
--body "" \
--json number --jq '.number')
echo "Created #$NUM: $title"
doneCapture the issue number at creation with --json number --jq '.number' so you can immediately add it to Projects v2 or link it elsewhere.
With Multi-line Body (Heredoc)
gh issue create \
--title "feat: Implement hybrid search" \
--body "$(cat <<'EOF'
## Description
Implement hybrid search combining BM25 and vector similarity.
## Acceptance Criteria
- [ ] HNSW index on chunks table
- [ ] RRF fusion algorithm
- [ ] 95%+ test pass rate
## Technical Notes
See PGVector skill for implementation details.
EOF
)"Using Templates
# Use repo template from .github/ISSUE_TEMPLATE/
gh issue create --template "bug_report.md"
# Use local file as body
gh issue create --title "..." --body-file ./issue-body.mdEditing Issues
# Add labels
gh issue edit 123 --add-label "high,backend"
# Remove labels
gh issue edit 123 --remove-label "low"
# Set milestone
gh issue edit 123 --milestone "Sprint 8"
# Assign users
gh issue edit 123 --add-assignee "username1,username2"
# Update title/body
gh issue edit 123 --title "New title" --body "Updated body"Listing and Filtering
# Open issues assigned to me
gh issue list --state open --assignee @me
# By label (multiple)
gh issue list --label "bug" --label "backend"
# By milestone
gh issue list --milestone "Sprint 5"
# Search with GitHub search syntax
gh issue list --search "status:success author:@me"
# Limit results
gh issue list --limit 20JSON Output for Scripting
# Get issue data as JSON
gh issue list --json number,title,labels,milestone,state
# Filter with jq
gh issue list --json number,labels \
--jq '[.[] | select(.labels[].name == "critical")] | .[].number'
# Get specific fields
gh issue view 123 --json title,body,labels,assigneesBulk Operations
Add Label to Multiple Issues
# Inline list
gh issue edit 23 34 45 --add-label "needs-review"
# From query result
gh issue list --label "stale" --json number --jq '.[].number' | \
xargs -I {} gh issue edit {} --add-label "deprecated"Close Multiple Issues
# Close with comment
gh issue list --label "duplicate" --json number --jq '.[].number' | \
while read num; do
gh issue close "$num" --comment "Closing as duplicate"
doneBulk Assign to Milestone
MILESTONE="Sprint 8"
ISSUES=$(gh issue list --label "sprint-8" --json number --jq '.[].number')
for issue in $ISSUES; do
gh issue edit "$issue" --milestone "$MILESTONE"
echo "Assigned #$issue to $MILESTONE"
doneIssue Comments
# Add comment
gh issue comment 123 --body "Working on this now"
# View comments
gh issue view 123 --comments
# Edit last comment (via web)
gh issue view 123 --webIssue Development Flow
# Create branch linked to issue
gh issue develop 123 --checkout
# This creates: username/issue-123-title-slug
# And checks it out locallySub-Issues
Native sub-issues are available but CLI support is limited:
# Install extension for sub-issue support
gh extension install yahsan2/gh-sub-issue
# Create sub-issue
gh sub-issue create 123 --title "Implement API endpoint"
# List sub-issues
gh sub-issue list 123Alternative: Use GraphQL
gh api graphql -f query='
mutation {
addSubIssue(input: {
parentId: "I_kwDOABCD1234"
subIssueId: "I_kwDOABCD5678"
}) {
parentIssue { title }
subIssue { title }
}
}
'Transfer and Pin Issues
# Move issue to another repo
gh issue transfer 123 owner/new-repo
# Pin to repo (max 3)
gh issue pin 123
# Unpin
gh issue unpin 123Common Patterns
Create Issue and Get Number
ISSUE_URL=$(gh issue create --title "..." --body "..." --json url --jq '.url')
ISSUE_NUM=$(echo "$ISSUE_URL" | grep -o '[0-9]*$')
echo "Created issue #$ISSUE_NUM"Find Untriaged Issues
# Issues with no labels
gh issue list --json number,title,labels \
--jq '[.[] | select(.labels | length == 0)]'Issue Statistics
# Count by label
gh issue list --state all --json labels \
--jq '[.[].labels[].name] | group_by(.) | map({label: .[0], count: length}) | sort_by(-.count)'Milestone Api
Milestone API Reference
GitHub CLI has NO native milestone commands. Use gh api REST calls for all milestone operations.
Critical footgun — NAME vs NUMBER:
gh issue edit --milestone/gh issue list --milestone→ accepts milestone NAME (string, e.g."Sprint 8")gh api repos/:owner/:repo/milestones/:number→ accepts milestone NUMBER (integer, e.g.5)These are different identifiers. Passing a number to
--milestonesilently fails. To get a number from a name:gh api repos/:owner/:repo/milestones --jq '.[] | select(.title=="Sprint 8") | .number'
API Endpoints
| Operation | Method | Endpoint |
|---|---|---|
| List | GET | /repos/:owner/:repo/milestones |
| Get | GET | /repos/:owner/:repo/milestones/:number |
| Create | POST | /repos/:owner/:repo/milestones |
| Update | PATCH | /repos/:owner/:repo/milestones/:number |
| Delete | DELETE | /repos/:owner/:repo/milestones/:number |
Create Milestone
# Minimal
gh api -X POST repos/:owner/:repo/milestones \
-f title="Sprint 10"
# Full options
gh api -X POST repos/:owner/:repo/milestones \
-f title="Sprint 10: Performance" \
-f state="open" \
-f description="Focus on frontend performance optimization" \
-f due_on="2026-03-15T00:00:00Z"
# Create and capture number
MILESTONE_NUM=$(gh api -X POST repos/:owner/:repo/milestones \
-f title="Sprint 8" \
--jq '.number')
echo "Created milestone #$MILESTONE_NUM"List Milestones
# All open (default)
gh api repos/:owner/:repo/milestones
# All milestones (including closed)
gh api "repos/:owner/:repo/milestones?state=all"
# Closed only
gh api "repos/:owner/:repo/milestones?state=closed"
# Sorted by due date
gh api "repos/:owner/:repo/milestones?sort=due_on&direction=asc"
# With jq formatting
gh api repos/:owner/:repo/milestones --jq '
.[] | {
number,
title,
state,
due: .due_on,
progress: "\(.closed_issues)/\(.open_issues + .closed_issues)"
}'
# Progress summary
gh api repos/:owner/:repo/milestones --jq '.[] | "\(.title): \(.closed_issues)/\(.open_issues + .closed_issues) done"'Get Single Milestone
gh api repos/:owner/:repo/milestones/1Update Milestone
# Change title
gh api -X PATCH repos/:owner/:repo/milestones/1 \
-f title="New Title"
# Update description
gh api -X PATCH repos/:owner/:repo/milestones/1 \
-f description="Updated scope: includes auth improvements"
# Update due date
gh api -X PATCH repos/:owner/:repo/milestones/1 \
-f due_on="2026-04-01T00:00:00Z"Close/Reopen Milestone
# Close milestone
gh api -X PATCH repos/:owner/:repo/milestones/5 -f state=closed
# Reopen milestone
gh api -X PATCH repos/:owner/:repo/milestones/5 -f state=openDelete Milestone
# Warning: removes milestone from all issues!
gh api -X DELETE repos/:owner/:repo/milestones/1Workflow Patterns
Sprint Workflow
# 1. Create sprint milestone
gh api -X POST repos/:owner/:repo/milestones \
-f title="Sprint 8: Auth & Performance" \
-f due_on="2026-02-14T00:00:00Z"
# 2. Assign issues to sprint
gh issue edit 123 124 125 --milestone "Sprint 8: Auth & Performance"
# 3. Check progress mid-sprint
gh api repos/:owner/:repo/milestones --jq '
.[] | select(.title | contains("Sprint 8")) |
"Progress: \(.closed_issues)/\(.open_issues + .closed_issues) (\((.closed_issues / (.open_issues + .closed_issues) * 100) | floor)%)"'
# 4. Close sprint when done
MILESTONE_NUM=$(gh api repos/:owner/:repo/milestones --jq '.[] | select(.title | contains("Sprint 8")) | .number')
gh api -X PATCH repos/:owner/:repo/milestones/$MILESTONE_NUM -f state=closedRelease Workflow
# 1. Create release milestone
gh api -X POST repos/:owner/:repo/milestones \
-f title="v2.0.0" \
-f description="Major release: New auth system, performance improvements" \
-f due_on="2026-03-01T00:00:00Z"
# 2. Tag issues for release
gh issue list --milestone "v2.0.0" --json number,title --jq '.[] | "#\(.number): \(.title)"'
# 3. When ready, close and create release
gh api -X PATCH repos/:owner/:repo/milestones/10 -f state=closed
gh release create v2.0.0 --generate-notesUseful Aliases
Add to ~/.config/gh/config.yml:
aliases:
ms: api repos/:owner/:repo/milestones --jq '.[] | "#\(.number) \(.title) [\(.state)] \(.closed_issues)/\(.open_issues+.closed_issues)"'
ms-create: '!f() { gh api -X POST repos/:owner/:repo/milestones -f title="$1" ${2:+-f due_on="$2"} ${3:+-f description="$3"}; }; f'
ms-close: '!f() { gh api -X PATCH repos/:owner/:repo/milestones/$1 -f state=closed; }; f'
ms-open: '!f() { gh api -X PATCH repos/:owner/:repo/milestones/$1 -f state=open; }; f'
ms-progress: '!f() { gh api repos/:owner/:repo/milestones/$1 --jq "\"Progress: \\(.closed_issues)/\\(.open_issues + .closed_issues) (\\((.closed_issues * 100 / (.open_issues + .closed_issues)) | floor)%)\""; }; f'Usage:
gh ms # List all
gh ms-create "Sprint 11" # Create
gh ms-create "v2.0" "2026-04-01" # With due date
gh ms-close 5 # Close
gh ms-progress 1 # Check progressBest Practices
- Use ISO 8601 dates -
YYYY-MM-DDTHH:MM:SSZformat for due_on - Meaningful titles - Include sprint number and focus area
- Close on time - Even with open issues, close at deadline
- Don't delete - Close instead to preserve history
- Link to Projects - Use Projects v2 Iteration field for cross-repo tracking
Pr Workflows
Pull Request Workflows
Creating PRs
Basic Creation
# Interactive (opens editor)
gh pr create
# Non-interactive with auto-fill from commits
gh pr create --fill
# Explicit title and body
gh pr create \
--title "feat(#123): Add hybrid search with PGVector" \
--body "Description..." \
--base dev \
--head feature/pgvector-searchFull PR Creation Pattern
gh pr create \
--title "feat(#${ISSUE_NUM}): Implement Langfuse tracing" \
--body "$(cat <<'EOF'
## Summary
- Added @observe decorator to workflow functions
- Implemented CallbackHandler for LangChain
- Added session and user tracking
## Changes
- `backend/app/shared/services/langfuse/` - New Langfuse client
- `backend/app/workflows/nodes/` - Added tracing decorators
- `backend/tests/unit/services/` - Langfuse unit tests
## Test Plan
- [ ] Unit tests pass (`poetry run pytest tests/unit/`)
- [ ] Integration test with real Langfuse instance
- [ ] Verify traces appear in Langfuse UI
Closes #372
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
EOF
)" \
--base dev \
--label "enhancement,backend" \
--assignee "@me" \
--reviewer "teammate"Using Body File
gh pr create --title "..." --body-file pr-description.mdPR Checks and Status
View Check Status
# List all checks
gh pr checks 456
# Watch checks in real-time
gh pr checks 456 --watch
# Wait for specific check
gh pr checks 456 --watch --fail-fastJSON Output for Automation
# Get check status
gh pr checks 456 --json name,state,conclusion
# Check if all passed
gh pr checks 456 --json conclusion \
--jq 'all(.[].conclusion == "SUCCESS")'Wait for Checks Pattern
PR_NUMBER=456
while true; do
STATUS=$(gh pr view $PR_NUMBER --json statusCheckRollupState --jq '.statusCheckRollupState')
case "$STATUS" in
"SUCCESS")
echo "All checks passed!"
break
;;
"FAILURE")
echo "Checks failed!"
gh pr checks $PR_NUMBER
exit 1
;;
*)
echo "Waiting... (status: $STATUS)"
sleep 30
;;
esac
donePR Reviews
Requesting Reviews
# Request review
gh pr edit 456 --add-reviewer "username1,username2"
# Remove reviewer
gh pr edit 456 --remove-reviewer "username"Submitting Reviews
# Approve
gh pr review 456 --approve
# Approve with comment
gh pr review 456 --approve --body "LGTM! Clean implementation."
# Request changes
gh pr review 456 --request-changes --body "Need tests for edge cases"
# Comment without approval/rejection
gh pr review 456 --comment --body "Nice refactoring!"View Review Status
# Get review decision
gh pr view 456 --json reviewDecision
# List reviews
gh pr view 456 --json reviews \
--jq '.reviews[] | "\(.author.login): \(.state)"'Merging PRs
Merge Strategies
# Merge commit (default)
gh pr merge 456 --merge
# Squash merge (recommended for clean history)
gh pr merge 456 --squash
# Rebase merge
gh pr merge 456 --rebase
# With branch deletion
gh pr merge 456 --squash --delete-branchAuto-Merge
# Enable auto-merge (merges when checks pass + approved)
gh pr merge 456 --auto --squash --delete-branch
# Disable auto-merge
gh pr merge 456 --disable-autoAdmin Merge (Bypass Protections)
# Bypass branch protection rules (requires admin)
gh pr merge 456 --admin --squashSafe Merge Pattern
#!/bin/bash
PR_NUMBER=$1
# 1. Verify checks passed
if ! gh pr view $PR_NUMBER --json statusCheckRollupState \
--jq '.statusCheckRollupState == "SUCCESS"' | grep -q true; then
echo "ERROR: Checks not passed"
gh pr checks $PR_NUMBER
exit 1
fi
# 2. Verify approved
APPROVED=$(gh pr view $PR_NUMBER --json reviewDecision --jq '.reviewDecision')
if [[ "$APPROVED" != "APPROVED" ]]; then
echo "ERROR: PR not approved (status: $APPROVED)"
exit 1
fi
# 3. Verify mergeable
MERGEABLE=$(gh pr view $PR_NUMBER --json mergeable --jq '.mergeable')
if [[ "$MERGEABLE" != "MERGEABLE" ]]; then
echo "ERROR: PR has conflicts"
exit 1
fi
# 4. Merge
gh pr merge $PR_NUMBER --squash --delete-branch
echo "Successfully merged PR #$PR_NUMBER"PR Comments
# Add comment
gh pr comment 456 --body "Addressed review feedback in latest commit"
# View comments
gh pr view 456 --commentsCheckout and Edit
# Checkout PR locally
gh pr checkout 456
# Edit PR metadata
gh pr edit 456 --title "New title" --add-label "urgent"
# Close without merging
gh pr close 456 --comment "Superseded by #789"
# Reopen
gh pr reopen 456PR Listing and Search
# My open PRs
gh pr list --author @me --state open
# PRs needing my review
gh pr list --search "review-requested:@me"
# Ready to merge
gh pr list --json number,title,reviewDecision,statusCheckRollupState \
--jq '[.[] | select(.reviewDecision == "APPROVED" and .statusCheckRollupState == "SUCCESS")]'
# Draft PRs
gh pr list --draftConvert Draft to Ready
# Mark ready for review
gh pr ready 456
# Convert to draft
gh pr ready 456 --undoPR Diff and Files
# View diff
gh pr diff 456
# List changed files
gh pr view 456 --json files --jq '.files[].path'
# View specific file
gh pr diff 456 -- path/to/file.pyCommon Patterns
Create PR from Current Branch
# Push and create PR in one flow
git push -u origin $(git branch --show-current) && \
gh pr create --fill --base devFind Stale PRs
# PRs not updated in 7 days
gh pr list --json number,title,updatedAt \
--jq '[.[] | select(.updatedAt < (now - 604800 | todate))]'PR Statistics
# Average time to merge
gh pr list --state merged --limit 20 --json createdAt,mergedAt \
--jq '[.[] | (.mergedAt | fromdateiso8601) - (.createdAt | fromdateiso8601)] | add / length / 3600 | "Average: \(.) hours"'Projects V2
GitHub Projects v2
Overview
GitHub Projects v2 uses custom fields for advanced project management. The gh project commands provide basic operations, but setting custom fields requires GraphQL.
Basic Project Commands
# List projects
gh project list --owner @me
# View project
gh project view 1 --owner @me
# List fields
gh project field-list 1 --owner @me --format json
# Add item to project
gh project item-add 1 --owner @me \
--url "https://github.com/org/repo/issues/123"
# Remove item from project
gh project item-delete 1 --owner @me --id "PVTI_abc123"Setting Custom Fields (GraphQL)
Set Single Select Field (Status, Priority, etc.)
PROJECT_ID="PVT_kwHOAS8tks4BIL_t"
ITEM_ID="PVTI_..." # From item-add result
FIELD_ID="PVTSSF_lAHOAS8tks4BIL_tzg4uOTk"
OPTION_ID="92ee1ecd" # Option value ID
gh api graphql -f query='
mutation($projectId: ID!, $itemId: ID!, $fieldId: ID!, $optionId: String!) {
updateProjectV2ItemFieldValue(input: {
projectId: $projectId
itemId: $itemId
fieldId: $fieldId
value: { singleSelectOptionId: $optionId }
}) {
projectV2Item {
id
}
}
}
' \
-f projectId="$PROJECT_ID" \
-f itemId="$ITEM_ID" \
-f fieldId="$FIELD_ID" \
-f optionId="$OPTION_ID"Set Text Field
gh api graphql -f query='
mutation($projectId: ID!, $itemId: ID!, $fieldId: ID!, $text: String!) {
updateProjectV2ItemFieldValue(input: {
projectId: $projectId
itemId: $itemId
fieldId: $fieldId
value: { text: $text }
}) {
projectV2Item { id }
}
}
' \
-f projectId="$PROJECT_ID" \
-f itemId="$ITEM_ID" \
-f fieldId="$TEXT_FIELD_ID" \
-f text="Custom value"Set Number Field
gh api graphql -f query='
mutation($projectId: ID!, $itemId: ID!, $fieldId: ID!, $number: Float!) {
updateProjectV2ItemFieldValue(input: {
projectId: $projectId
itemId: $itemId
fieldId: $fieldId
value: { number: $number }
}) {
projectV2Item { id }
}
}
' \
-f projectId="$PROJECT_ID" \
-f itemId="$ITEM_ID" \
-f fieldId="$NUMBER_FIELD_ID" \
-F number=5Complete Workflow: Create Issue + Add to Project
#!/bin/bash
# create-and-track.sh
TITLE="$1"
BODY="$2"
LABELS="${3:-enhancement}"
STATUS_OPTION="${4:-19303ae5}" # Default: ready
# Configuration
PROJECT_NUMBER=1
PROJECT_OWNER="username"
PROJECT_ID="PVT_..."
STATUS_FIELD_ID="PVTSSF_..."
REPO="org/repo"
# 1. Create issue
ISSUE_URL=$(gh issue create \
--repo "$REPO" \
--title "$TITLE" \
--body "$BODY" \
--label "$LABELS" \
--json url --jq '.url')
echo "Created: $ISSUE_URL"
# 2. Add to project
ITEM_ID=$(gh project item-add $PROJECT_NUMBER \
--owner $PROJECT_OWNER \
--url "$ISSUE_URL" \
--format json | jq -r '.id')
echo "Added to project: $ITEM_ID"
# 3. Set status
gh api graphql -f query='
mutation($projectId: ID!, $itemId: ID!, $fieldId: ID!, $optionId: String!) {
updateProjectV2ItemFieldValue(input: {
projectId: $projectId
itemId: $itemId
fieldId: $fieldId
value: { singleSelectOptionId: $optionId }
}) {
projectV2Item { id }
}
}
' \
-f projectId="$PROJECT_ID" \
-f itemId="$ITEM_ID" \
-f fieldId="$STATUS_FIELD_ID" \
-f optionId="$STATUS_OPTION"
echo "Set status"Query Project Items
Get All Items with Status
gh api graphql -f query='
query($owner: String!, $number: Int!) {
user(login: $owner) {
projectV2(number: $number) {
items(first: 50) {
nodes {
id
content {
... on Issue {
number
title
}
... on PullRequest {
number
title
}
}
fieldValues(first: 10) {
nodes {
... on ProjectV2ItemFieldSingleSelectValue {
name
field { ... on ProjectV2SingleSelectField { name } }
}
}
}
}
}
}
}
}
' -f owner="username" -F number=1Get Items by Status
# Get all "In Development" items
gh api graphql -f query='
query($owner: String!, $number: Int!) {
user(login: $owner) {
projectV2(number: $number) {
items(first: 100) {
nodes {
content {
... on Issue { number title }
}
fieldValues(first: 5) {
nodes {
... on ProjectV2ItemFieldSingleSelectValue {
name
}
}
}
}
}
}
}
}
' -f owner="username" -F number=1 \
--jq '.data.user.projectV2.items.nodes[] | select(.fieldValues.nodes[].name == "In Development") | .content'Discovering Field IDs
If you need to find field/option IDs for a project:
# Get all fields with options
gh api graphql -f query='
query($owner: String!, $number: Int!) {
user(login: $owner) {
projectV2(number: $number) {
id
fields(first: 20) {
nodes {
... on ProjectV2Field {
id
name
}
... on ProjectV2SingleSelectField {
id
name
options {
id
name
}
}
... on ProjectV2IterationField {
id
name
}
}
}
}
}
}
' -f owner="username" -F number=1 | jq '.data.user.projectV2'Move Item Between Statuses
# Helper function
move_to_status() {
local ITEM_ID="$1"
local STATUS_OPTION_ID="$2"
gh api graphql -f query='
mutation($projectId: ID!, $itemId: ID!, $fieldId: ID!, $optionId: String!) {
updateProjectV2ItemFieldValue(input: {
projectId: $projectId
itemId: $itemId
fieldId: $fieldId
value: { singleSelectOptionId: $optionId }
}) {
projectV2Item { id }
}
}
' \
-f projectId="$PROJECT_ID" \
-f itemId="$ITEM_ID" \
-f fieldId="$STATUS_FIELD_ID" \
-f optionId="$STATUS_OPTION_ID"
}
# Usage
move_to_status "PVTI_abc123" "92ee1ecd" # Move to In DevelopmentOrganization Projects
For organization-owned projects, use organization instead of user:
gh api graphql -f query='
query($org: String!, $number: Int!) {
organization(login: $org) {
projectV2(number: $number) {
id
title
}
}
}
' -f org="my-org" -F number=1Examples (1)
Automation Scripts
GitHub Automation Scripts
Ready-to-use scripts for common GitHub automation tasks.
Bulk Issue Operations
Add Label to Multiple Issues
#!/usr/bin/env bash
# Add label to all issues matching criteria
LABEL="needs-review"
QUERY="is:open label:bug"
gh issue list --search "$QUERY" --json number --jq '.[].number' | \
while read -r issue; do
echo "Adding '$LABEL' to #$issue"
gh issue edit "$issue" --add-label "$LABEL"
doneAssign Issues to Team Member
#!/usr/bin/env bash
# Assign unassigned issues in a milestone
MILESTONE="Sprint 8"
ASSIGNEE="@me"
gh issue list --milestone "$MILESTONE" --assignee "" --json number --jq '.[].number' | \
while read -r issue; do
echo "Assigning #$issue to $ASSIGNEE"
gh issue edit "$issue" --add-assignee "$ASSIGNEE"
doneClose Stale Issues
#!/usr/bin/env bash
# Close issues with no activity > 90 days
DAYS=90
CUTOFF=$(date -v-${DAYS}d +%Y-%m-%d 2>/dev/null || date -d "$DAYS days ago" +%Y-%m-%d)
gh issue list --state open --json number,updatedAt --jq \
".[] | select(.updatedAt < \"$CUTOFF\") | .number" | \
while read -r issue; do
echo "Closing stale issue #$issue"
gh issue close "$issue" --comment "Closing due to inactivity. Reopen if still relevant."
donePR Automation
Auto-Merge Approved PRs
#!/usr/bin/env bash
# Enable auto-merge for approved PRs with passing checks
gh pr list --json number,reviewDecision,statusCheckRollupState --jq \
'.[] | select(.reviewDecision == "APPROVED" and .statusCheckRollupState == "SUCCESS") | .number' | \
while read -r pr; do
echo "Enabling auto-merge for PR #$pr"
gh pr merge "$pr" --auto --squash --delete-branch
doneRequest Reviews from CODEOWNERS
#!/usr/bin/env bash
# Add reviewers based on changed files
PR_NUMBER=$1
# Get changed files
CHANGED=$(gh pr diff "$PR_NUMBER" --name-only)
# Map to reviewers (customize per team)
REVIEWERS=""
if echo "$CHANGED" | grep -q "^src/api/"; then
REVIEWERS="$REVIEWERS backend-team"
fi
if echo "$CHANGED" | grep -q "^src/components/"; then
REVIEWERS="$REVIEWERS frontend-team"
fi
if [[ -n "$REVIEWERS" ]]; then
gh pr edit "$PR_NUMBER" --add-reviewer $REVIEWERS
fiPR Status Dashboard
#!/usr/bin/env bash
# Generate PR status summary
echo "=== PR Status Dashboard ==="
echo ""
echo "## Ready to Merge"
gh pr list --json number,title,author --jq \
'.[] | "- #\(.number) \(.title) (@\(.author.login))"' \
--search "review:approved status:success"
echo ""
echo "## Needs Review"
gh pr list --json number,title,author --jq \
'.[] | "- #\(.number) \(.title) (@\(.author.login))"' \
--search "review:none"
echo ""
echo "## Changes Requested"
gh pr list --json number,title,author --jq \
'.[] | "- #\(.number) \(.title) (@\(.author.login))"' \
--search "review:changes-requested"Milestone Management
Milestone Progress Report
#!/usr/bin/env bash
# Generate milestone progress report
echo "=== Milestone Progress ==="
echo ""
gh api repos/:owner/:repo/milestones --jq '.[] |
"## \(.title)
Due: \(.due_on // "No due date")
Progress: \(.closed_issues)/\(.open_issues + .closed_issues) issues (\((.closed_issues * 100 / ((.open_issues + .closed_issues) | if . == 0 then 1 else . end)) | floor)%)
"'Move Issues to Next Sprint
#!/usr/bin/env bash
# Move open issues from current to next milestone
CURRENT="Sprint 7"
NEXT="Sprint 8"
gh issue list --milestone "$CURRENT" --state open --json number --jq '.[].number' | \
while read -r issue; do
echo "Moving #$issue to $NEXT"
gh issue edit "$issue" --milestone "$NEXT"
doneCreate Sprint Milestone
#!/usr/bin/env bash
# Create milestone with due date
SPRINT_NUM=$1
WEEKS=${2:-2}
DUE_DATE=$(date -v+${WEEKS}w +%Y-%m-%dT00:00:00Z 2>/dev/null || \
date -d "+$WEEKS weeks" +%Y-%m-%dT00:00:00Z)
gh api -X POST repos/:owner/:repo/milestones \
-f title="Sprint $SPRINT_NUM" \
-f description="Sprint $SPRINT_NUM goals and deliverables" \
-f due_on="$DUE_DATE"
echo "Created Sprint $SPRINT_NUM (due $DUE_DATE)"Cross-Repo Operations
Sync Labels Across Repos
#!/usr/bin/env bash
# Copy labels from source repo to target repos
SOURCE_REPO="org/main-repo"
TARGET_REPOS=("org/api" "org/frontend" "org/docs")
# Get labels from source
LABELS=$(gh label list --repo "$SOURCE_REPO" --json name,color,description)
for repo in "${TARGET_REPOS[@]}"; do
echo "Syncing labels to $repo"
echo "$LABELS" | jq -c '.[]' | while read -r label; do
NAME=$(echo "$label" | jq -r '.name')
COLOR=$(echo "$label" | jq -r '.color')
DESC=$(echo "$label" | jq -r '.description // ""')
gh label create "$NAME" --repo "$repo" --color "$COLOR" --description "$DESC" 2>/dev/null || \
gh label edit "$NAME" --repo "$repo" --color "$COLOR" --description "$DESC"
done
doneFind Issues Across Repos
#!/usr/bin/env bash
# Search issues across organization
ORG="my-org"
QUERY="is:open label:critical"
gh search issues --owner "$ORG" "$QUERY" --json repository,number,title --jq \
'.[] | "\(.repository.nameWithOwner)#\(.number): \(.title)"'Rate Limit Handling
#!/usr/bin/env bash
# Check rate limits before bulk operations
check_rate_limit() {
REMAINING=$(gh api rate_limit --jq '.rate.remaining')
if [[ "$REMAINING" -lt 100 ]]; then
RESET=$(gh api rate_limit --jq '.rate.reset')
WAIT=$((RESET - $(date +%s)))
echo "Rate limit low ($REMAINING). Waiting ${WAIT}s..."
sleep "$WAIT"
fi
}
# Use in loops
for issue in $(gh issue list --json number --jq '.[].number'); do
check_rate_limit
gh issue edit "$issue" --add-label "processed"
doneRelated
Git Workflow
Complete git workflow patterns including GitHub Flow branching, atomic commits with interactive staging, merge and rebase strategies, and recovery operations using reflog. Essential patterns for clean history. Use when managing branches, defining branching strategy, or recovering git history.
Golden Dataset
Golden dataset lifecycle patterns for curation, versioning, quality validation, and CI integration. Use when building evaluation datasets, managing dataset versions, validating quality scores, or integrating golden tests into pipelines.
Last updated on