
Mastering Github Cli
- 1 installs
- 46 repo stars
- Updated March 18, 2026
- spillwavesolutions/agent_rulez
Helps with ai & agent building tasks during AI-assisted development.
About
mastering-github-cli is a Claude Code skill for ai & agent building. It helps solo builders move faster with AI-assisted coding.
- mastering-github-cli
- AI & Agent Building
- AI-coding skill
Mastering Github Cli by the numbers
- 1 all-time installs (skills.sh)
- Ranked #14,098 of 16,546 AI & Agent Building skills by installs in the Skillselion catalog
- Data as of Jul 29, 2026 (Skillselion catalog sync)
npx skills add https://github.com/spillwavesolutions/agent_rulez --skill mastering-github-cliAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 1 |
|---|---|
| repo stars | ★ 46 |
| Last updated | March 18, 2026 |
| Repository | spillwavesolutions/agent_rulez ↗ |
What it does
Helps with ai & agent building tasks during AI-assisted development.
Files
Mastering GitHub CLI
Command-line interface for GitHub operations: search, monitoring, resource creation, workflow authoring, and automation.
Contents
---
Quick Start
Find repos with specific files/directories
gh search code "path:.skilz" --json repository --jq '.[].repository.fullName'
gh search code --filename SKILL.md
gh search code --filename Dockerfile --language pythonMonitor CI/CD
gh run list --workflow=CI --status=failure --limit 10
gh run watch 12345 --exit-status # Block until complete
gh run view 12345 --log-failed # Failed logs only
gh pr checks 123 --watch # PR CI statusCreate resources
gh pr create --title "Feature" --body "Description" --reviewer @user
gh issue create --title "Bug" --label bug,urgent --assignee @me
gh repo fork owner/repo --cloneTrigger and monitor workflow
gh workflow run deploy.yml -f environment=staging
sleep 5
RUN_ID=$(gh run list --workflow=deploy.yml --limit 1 --json databaseId --jq '.[0].databaseId')
gh run watch "$RUN_ID" --exit-status---
Command Reference
| Task | Command | Reference |
|---|---|---|
| Find repos with file | gh search code --filename FILE | search.md |
| Find repos with directory | gh search code "path:DIR" | search.md |
| List failed runs | gh run list --status=failure | monitoring.md |
| Watch run | gh run watch ID --exit-status | monitoring.md |
| Download artifacts | gh run download ID -n NAME | monitoring.md |
| Create PR | gh pr create --fill | resources.md |
| Check PR CI | gh pr checks --watch | monitoring.md |
| Trigger workflow | gh workflow run NAME.yml | automation.md |
| Fork repo | gh repo fork REPO --clone | resources.md |
| REST/GraphQL API | gh api repos/{owner}/{repo} | api.md |
JSON Output
gh pr list --json # Discover fields
gh pr list --json number,title,author,labels # Select fields
gh run list --json status --jq '.[] | select(.status=="completed")'Environment & Auth
| Variable | Purpose |
|---|---|
GH_TOKEN | Authentication token |
GH_REPO | Default owner/repo |
GH_HOST | GitHub Enterprise host |
GH_PROMPT_DISABLED=1 | Disable prompts (CI) |
Rate Limits
| Endpoint | Limit | Notes |
|---|---|---|
| REST API | 5,000/hour | Per authenticated user |
| Search API | 30/minute | All search endpoints |
| Code Search | 10/minute | More restrictive |
| Search results | 1,000 max | Use date partitioning for more |
---
Workflow Authoring
GitHub Actions workflow YAML patterns for CI/CD pipelines.
Basic Structure
name: CI Pipeline
on:
push:
branches: [main]
pull_request:
workflow_dispatch:
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: make build
- run: make testCaching Patterns
# Python/Poetry
- uses: actions/setup-python@v5
with:
python-version: '3.11'
- uses: actions/cache@v4
with:
path: .venv
key: venv-${{ runner.os }}-${{ hashFiles('poetry.lock') }}
# Node.js
- uses: actions/setup-node@v4
with:
node-version: '20'
cache: 'npm'Matrix Builds
strategy:
matrix:
os: [ubuntu-latest, windows-latest]
node: [18, 20]
fail-fast: falseOIDC Authentication (AWS/GCP)
permissions:
id-token: write
contents: read
steps:
- uses: aws-actions/configure-aws-credentials@v4
with:
role-to-assume: arn:aws:iam::123456789012:role/GitHubActionsRole
aws-region: us-east-1Full reference: references/workflow-authoring.md
Advanced Patterns
For enterprise CI/CD pipelines:
- Ephemeral PR Environments - Auto-create/destroy per PR
- Release Please - Automated semantic versioning
- PR Cleanup - Resource cleanup on close
- Testing Patterns - pytest, Gradle, Jest with coverage
- Deployment Status Checks - Wait for stack readiness
- Separate Build/Deploy Roles - Fine-grained OIDC permissions
- Multi-Stack Ordering - Foundation → Schemas → Application
Additional References:
- Workflow Summaries - Rich markdown output with $GITHUB_STEP_SUMMARY
- Container Security - Multi-stage builds, Trivy scanning, image tagging
- Security Scanning - CodeQL, Dependabot, IaC scanning (Checkov, tfsec)
- Troubleshooting - Debug mode, flaky tests, common issues
- Performance - Limits, runner specs, optimization tips
---
Scripts
Pre-built automation scripts with error handling and rate limit awareness.
| Script | Purpose | Usage |
|---|---|---|
find-repos-with-path.sh | Find repos containing specific paths | ./scripts/find-repos-with-path.sh .skilz [owner] |
wait-for-run.sh | Block until workflow completes | ./scripts/wait-for-run.sh RUN_ID [timeout] |
batch-search.sh | Search >1000 results via date partitioning | ./scripts/batch-search.sh "query" START END |
Exit Codes
| Script | 0 | 1 | 2 | 3 |
|---|---|---|---|---|
wait-for-run.sh | success | failure | cancelled | timeout |
find-repos-with-path.sh | success | error | - | - |
batch-search.sh | success | error | - | - |
---
Validation Checklist
Before completing a GitHub CLI task, verify:
- [ ] gh authenticated (`gh auth status`)
- [ ] Command syntax matches documentation
- [ ] Exit codes checked and handled appropriately
- [ ] Rate limits considered (code search: 10/min, repo search: 30/min)
- [ ] JSON output parsed correctly (if using --json)
- [ ] Artifacts downloaded to correct directory (if applicable)
- [ ] PR/Issue created with proper labels and assignees (if applicable)
- [ ] Workflow triggered and monitored to completion (if applicable)---
When Not to Use
This skill covers gh CLI and GitHub Actions workflow authoring. Do not use for:
- Git commands:
git push,git commit,git pull(use git directly) - GitHub web UI: Questions about github.com interface navigation
- GitHub Desktop: Different application entirely
- Direct curl/HTTP: API calls without
ghwrapper - GitHub mobile app: Mobile-specific features
# OS files
.DS_Store
Thumbs.db
# Editor files
*.swp
*.swo
*~
.vscode/
.idea/
# Temporary files
*.tmp
*.log
*.bak
# Script output
*.out
results/
# Environment
.env
.env.local
installed_at: '2026-01-22T04:23:48+00:00'
skill_id: git/mastering-github-cli
git_repo: https://github.com/SpillwaveSolutions/mastering-github-agent-skill
skill_path: /private/var/folders/tm/chrvt43s3rbdld20ghw1qtc40000gn/T/skilz-git-f4nz_47t
git_sha: ae5e218cf835b3f32a0a6416f9685671905c56a8
skilz_version: 1.9.1
install_mode: copy
canonical_path: null
Mastering GitHub CLI
A comprehensive Claude Code skill for GitHub CLI operations, CI/CD monitoring, workflow authoring, and automation.
Overview
This skill provides command-line interface mastery for GitHub operations including:
- Repository Search - Find repos by files, directories, code patterns
- CI/CD Monitoring - Watch workflow runs, check PR status, download artifacts
- Resource Creation - Create PRs, issues, repos, branches from command line
- Workflow Authoring - Write GitHub Actions YAML with caching, matrix builds, OIDC
- Automation - Trigger workflows, batch operations, API access
Installing with Skilz (Universal Installer)
The recommended way to install this skill across different AI coding agents is using the skilz universal installer.
Install Skilz
pip install skilzThis skill supports Agent Skill Standard which means it supports 14 plus coding agents including Claude Code, OpenAI Codex, Cursor and Gemini.
Git URL Options
You can use either -g or --git with HTTPS or SSH URLs:
# HTTPS URL
skilz install -g https://github.com/SpillwaveSolutions/mastering-github-agent-skill
# SSH URL
skilz install --git git@github.com:SpillwaveSolutions/mastering-github-agent-skill.gitClaude Code
Install to user home (available in all projects):
skilz install -g https://github.com/SpillwaveSolutions/mastering-github-agent-skillInstall to current project only:
skilz install -g https://github.com/SpillwaveSolutions/mastering-github-agent-skill --projectOpenCode
Install for OpenCode:
skilz install -g https://github.com/SpillwaveSolutions/mastering-github-agent-skill --agent opencodeProject-level install:
skilz install -g https://github.com/SpillwaveSolutions/mastering-github-agent-skill --project --agent opencodeGemini
Project-level install for Gemini:
skilz install -g https://github.com/SpillwaveSolutions/mastering-github-agent-skill --agent geminiOpenAI Codex
Install for OpenAI Codex:
skilz install -g https://github.com/SpillwaveSolutions/mastering-github-agent-skill --agent codexProject-level install:
skilz install -g https://github.com/SpillwaveSolutions/mastering-github-agent-skill --project --agent codexInstall from Skillzwave Marketplace
# Claude to user home dir ~/.claude/skills
skilz install SpillwaveSolutions_mastering-github-agent-skill/mastering-github-cli
# Claude skill in project folder ./claude/skills
skilz install SpillwaveSolutions_mastering-github-agent-skill/mastering-github-cli --project
# OpenCode install to user home dir ~/.config/opencode/skills
skilz install SpillwaveSolutions_mastering-github-agent-skill/mastering-github-cli --agent opencode
# OpenCode project level
skilz install SpillwaveSolutions_mastering-github-agent-skill/mastering-github-cli --agent opencode --project
# OpenAI Codex install to user home dir ~/.codex/skills
skilz install SpillwaveSolutions_mastering-github-agent-skill/mastering-github-cli
# OpenAI Codex project level ./.codex/skills
skilz install SpillwaveSolutions_mastering-github-agent-skill/mastering-github-cli --agent codex --project
# Gemini CLI (project level) -- only works with project level
skilz install SpillwaveSolutions_mastering-github-agent-skill/mastering-github-cli --agent geminiSee this site skill Listing to see how to install this exact skill to 14+ different coding agents.
Other Supported Agents
Skilz supports 14+ coding agents including Windsurf, Qwen Code, Aidr, and more.
For the full list of supported platforms, visit SkillzWave.ai/platforms or see the skilz-cli GitHub repository
Prerequisites
- GitHub CLI (
gh) installed - Authenticated:
gh auth login - Optional:
jqfor JSON processing
Quick Examples
# Find repos with specific files
gh search code --filename SKILL.md
# Monitor CI/CD
gh run list --workflow=CI --status=failure
gh pr checks 123 --watch
# Create resources
gh pr create --fill
gh issue create --title "Bug" --label bug
# Trigger workflow
gh workflow run deploy.yml -f environment=stagingStructure
mastering-github-cli/
├── SKILL.md # Main skill file with quick reference
├── README.md # This file
├── references/ # Detailed reference documentation
│ ├── search.md # Code and repository search
│ ├── monitoring.md # CI/CD monitoring commands
│ ├── resources.md # Creating PRs, issues, repos
│ ├── automation.md # Workflow triggers and batch ops
│ ├── api.md # REST/GraphQL API access
│ └── workflow-authoring.md # GitHub Actions YAML patterns
└── scripts/ # Ready-to-use automation scripts
├── find-repos-with-path.sh # Find repos with specific paths
├── wait-for-run.sh # Wait for workflow completion
└── batch-search.sh # Search >1000 resultsRate Limits
| Endpoint | Limit | Notes |
|---|---|---|
| REST API | 5,000/hour | Per authenticated user |
| Search API | 30/minute | All search endpoints |
| Code Search | 10/minute | More restrictive |
License
MIT
---
<a href="https://skillzwave.ai/">Largest Agentic Marketplace for AI Agent Skills</a> and <a href="https://spillwave.com/">SpillWave: Leaders in AI Agent Development.</a>
API Access Reference
Direct GitHub API access via gh api for operations not covered by built-in commands.
Contents
---
REST API Basics
Syntax
gh api <endpoint> [flags]| Flag | Description | Example |
|---|---|---|
-X | HTTP method | -X POST, -X DELETE |
-f | String field | -f title="Issue" |
-F | Typed field (file, bool, int) | -F draft=true |
-H | Header | -H "Accept: application/json" |
--jq | jq filter | --jq '.name' |
--template | Go template | --template '{{.name}}' |
--paginate | Auto-paginate | --paginate |
--cache | Cache duration | --cache 3600s |
--silent | No output | --silent |
HTTP Methods
# GET (default)
gh api repos/{owner}/{repo}
# POST
gh api repos/{owner}/{repo}/issues -f title="Bug" -f body="Description"
# PATCH
gh api repos/{owner}/{repo} -X PATCH -f description="Updated"
# PUT
gh api repos/{owner}/{repo}/topics -X PUT -f names='["topic1","topic2"]'
# DELETE
gh api repos/{owner}/{repo}/issues/1/labels/bug -X DELETEField types
# String field
-f title="My Issue"
# Integer field
-F per_page=100
# Boolean field
-F draft=true
# JSON field
-f names='["a","b","c"]'
# File contents
-F body=@file.md
# Null value
-F value=nullPath parameters
# Use {owner} and {repo} as placeholders
gh api repos/{owner}/{repo}
# They resolve from current repo, or specify:
gh api repos/octocat/hello-world
# With GH_REPO set
export GH_REPO=owner/repo
gh api repos/{owner}/{repo} # Uses owner/repo---
GraphQL Patterns
Basic query
gh api graphql -f query='
query {
viewer {
login
name
}
}
'With variables
gh api graphql -f query='
query($owner: String!, $name: String!) {
repository(owner: $owner, name: $name) {
stargazerCount
description
}
}
' -f owner=octocat -f name=hello-worldMutations
gh api graphql -f query='
mutation($id: ID!) {
addStar(input: {starrableId: $id}) {
starrable {
... on Repository {
nameWithOwner
}
}
}
}
' -f id=REPO_NODE_IDPagination in GraphQL
gh api graphql --paginate -f query='
query($endCursor: String) {
viewer {
repositories(first: 100, after: $endCursor) {
pageInfo {
hasNextPage
endCursor
}
nodes {
nameWithOwner
stargazerCount
}
}
}
}
'Common fragments
# Repository info
gh api graphql -f query='
query($owner: String!, $name: String!) {
repository(owner: $owner, name: $name) {
nameWithOwner
description
stargazerCount
forkCount
primaryLanguage { name }
licenseInfo { spdxId }
defaultBranchRef { name }
}
}
' -f owner=owner -f name=repo---
Pagination
REST pagination
# Auto-paginate all results
gh api --paginate repos/{owner}/{repo}/issues
# With jq processing
gh api --paginate repos/{owner}/{repo}/issues --jq '.[].title'
# Slurp into single array
gh api --paginate --slurp repos/{owner}/{repo}/issues | jq 'flatten'Manual pagination
# First page
gh api repos/{owner}/{repo}/issues -F per_page=100 -F page=1
# Subsequent pages
gh api repos/{owner}/{repo}/issues -F per_page=100 -F page=2Handling large datasets
# Paginate and process incrementally
page=1
while true; do
results=$(gh api repos/{owner}/{repo}/issues -F per_page=100 -F page=$page)
count=$(echo "$results" | jq 'length')
[ "$count" -eq 0 ] && break
echo "$results" | jq '.[] | .title'
((page++))
doneGraphQL cursor pagination
# Collect all with cursor
cursor=""
while true; do
if [ -z "$cursor" ]; then
result=$(gh api graphql -f query='...(first: 100)...')
else
result=$(gh api graphql -f query='...(first: 100, after: $cursor)...' -f cursor="$cursor")
fi
# Process nodes
echo "$result" | jq '.data.repository.issues.nodes[]'
# Check for next page
has_next=$(echo "$result" | jq -r '.data.repository.issues.pageInfo.hasNextPage')
[ "$has_next" != "true" ] && break
cursor=$(echo "$result" | jq -r '.data.repository.issues.pageInfo.endCursor')
done---
Caching
Cache responses
# Cache for 1 hour
gh api --cache 3600s repos/{owner}/{repo}
# Cache for 1 hour (alternative)
gh api --cache 1h repos/{owner}/{repo}
# Cache for 24 hours
gh api --cache 24h repos/{owner}/{repo}/releasesWhen to cache
- Repo metadata that doesn't change often
- Release information
- User/org profiles
- Rate limit sensitive operations
Cache considerations
- Cache is per-URL
- Mutations (POST/PATCH/DELETE) bypass cache
- Use for read-heavy operations
---
Common Endpoints
Repository
# Get repo
gh api repos/{owner}/{repo}
# Update repo
gh api repos/{owner}/{repo} -X PATCH -f description="New desc"
# Get topics
gh api repos/{owner}/{repo}/topics
# Set topics
gh api repos/{owner}/{repo}/topics -X PUT -f names='["topic1","topic2"]'
# Get languages
gh api repos/{owner}/{repo}/languages
# Get contributors
gh api repos/{owner}/{repo}/contributorsIssues & PRs
# List issues
gh api repos/{owner}/{repo}/issues
# Create issue
gh api repos/{owner}/{repo}/issues -f title="Bug" -f body="Details"
# Update issue
gh api repos/{owner}/{repo}/issues/1 -X PATCH -f state="closed"
# List PR files
gh api repos/{owner}/{repo}/pulls/1/files
# Merge PR
gh api repos/{owner}/{repo}/pulls/1/merge -X PUT -f merge_method="squash"Workflows & Runs
# List workflows
gh api repos/{owner}/{repo}/actions/workflows
# List runs
gh api repos/{owner}/{repo}/actions/runs
# Get run
gh api repos/{owner}/{repo}/actions/runs/{run_id}
# Download logs
gh api repos/{owner}/{repo}/actions/runs/{run_id}/logs > logs.zip
# List artifacts
gh api repos/{owner}/{repo}/actions/runs/{run_id}/artifacts
# Trigger workflow
gh api repos/{owner}/{repo}/actions/workflows/{workflow_id}/dispatches \
-f ref="main" -f inputs='{"environment":"staging"}'Users & Orgs
# Current user
gh api user
# Specific user
gh api users/{username}
# User repos
gh api users/{username}/repos
# Org info
gh api orgs/{org}
# Org repos
gh api orgs/{org}/repos
# Org members
gh api orgs/{org}/membersSearch
# Search repos
gh api search/repositories -f q='language:python stars:>100'
# Search code
gh api search/code -f q='filename:Dockerfile org:myorg'
# Search issues
gh api search/issues -f q='repo:owner/name is:issue is:open'---
Advanced Patterns
Conditional requests (ETag)
# Get ETag from first request
response=$(gh api repos/{owner}/{repo} -i)
etag=$(echo "$response" | grep -i etag | cut -d' ' -f2 | tr -d '\r')
# Use ETag for conditional request
gh api repos/{owner}/{repo} -H "If-None-Match: $etag"
# Returns 304 if unchangedPreview headers
# Some APIs require preview headers
gh api repos/{owner}/{repo}/topics \
-H "Accept: application/vnd.github.mercy-preview+json"Rate limit info from headers
# Get headers with response
gh api repos/{owner}/{repo} -i 2>&1 | grep -i "x-ratelimit"Batch with jq
# Get multiple repos in one script
repos=("owner/repo1" "owner/repo2" "owner/repo3")
for repo in "${repos[@]}"; do
gh api "repos/$repo" --jq '{name: .full_name, stars: .stargazers_count}'
done | jq -s '.'Error handling
# Check for errors in response
if response=$(gh api repos/nonexistent/repo 2>&1); then
echo "Success: $response"
else
echo "Error: $response"
fi
# Parse error message
gh api repos/nonexistent/repo 2>&1 | jq -r '.message // "Unknown error"'Creating complex objects
# Create issue with labels and assignees
gh api repos/{owner}/{repo}/issues \
-f title="Complex issue" \
-f body="Description here" \
-f labels='["bug","priority:high"]' \
-f assignees='["user1","user2"]' \
-F milestone=1Webhooks
# List webhooks
gh api repos/{owner}/{repo}/hooks
# Create webhook
gh api repos/{owner}/{repo}/hooks \
-f name="web" \
-f config='{"url":"https://example.com/webhook","content_type":"json"}' \
-f events='["push","pull_request"]' \
-F active=true
# Delete webhook
gh api repos/{owner}/{repo}/hooks/{hook_id} -X DELETEAutomation Patterns Reference
Comprehensive reference for scripting, batch operations, error handling, and agentic workflows with GitHub CLI.
Contents
- Workflow Triggering
- JSON Output Patterns
- Batch Operations
- Error Handling
- Retry Patterns
- Rate Limit Handling
- Environment Setup
- Complete Workflow Examples
---
Workflow Triggering
Basic triggering
gh workflow run <workflow> [flags]| Flag | Description | Example |
|---|---|---|
-f | Input field | -f environment=staging |
-F | Input from file | -F config=@config.json |
--json | Read inputs from stdin | --json |
--ref | Branch/tag/SHA | --ref feature-branch |
Examples
# Simple trigger
gh workflow run deploy.yml
# With inputs
gh workflow run deploy.yml -f environment=staging -f debug=true
# From specific branch
gh workflow run build.yml --ref develop
# JSON inputs from stdin
echo '{"environment":"prod","version":"1.2.3"}' | gh workflow run deploy.yml --json
# JSON inputs from file
gh workflow run deploy.yml --json < inputs.jsonWorkflow management
# List workflows
gh workflow list
gh workflow list --json id,name,state
# View workflow
gh workflow view deploy.yml
gh workflow view deploy.yml --yaml # Show YAML definition
# Enable/disable
gh workflow enable deploy.yml
gh workflow disable legacy.ymlRun management
# Rerun failed jobs only
gh run rerun 12345 --failed
# Rerun with debug logging
gh run rerun 12345 --debug
# Rerun specific jobs
gh run rerun 12345 --job job_id
# Cancel run
gh run cancel 12345---
JSON Output Patterns
Discovering available fields
# List all fields (no value = show available)
gh pr list --json
gh run list --json
gh issue list --json
gh search repos --jsonSelecting fields
# Single field
gh pr list --json number
# Multiple fields
gh pr list --json number,title,author,labels
# Nested fields
gh pr list --json author --jq '.[].author.login'jq filtering patterns
# Select by condition
gh run list --json status,conclusion \
--jq '.[] | select(.status == "completed")'
# Filter and extract
gh pr list --json number,labels \
--jq '.[] | select(.labels | any(.name == "bug")) | .number'
# Count
gh issue list --json number --jq 'length'
# Unique values
gh search code "path:.skilz" --json repository \
--jq '[.[].repository.fullName] | unique'
# Group by
gh pr list --json state --jq 'group_by(.state) | map({state: .[0].state, count: length})'
# Sort
gh search repos "cli" --json stargazersCount,fullName \
--jq 'sort_by(-.stargazersCount) | .[:10]'
# Map/transform
gh run list --json databaseId,conclusion \
--jq 'map({id: .databaseId, result: .conclusion})'Template formatting
# Go template
gh pr list --json number,title,updatedAt --template \
'{{range .}}{{tablerow .number .title (timeago .updatedAt)}}{{end}}'
# Table output
gh pr list --json number,title,author --template \
'{{tablerow "PR" "Title" "Author"}}{{range .}}{{tablerow .number .title .author.login}}{{end}}'Exporting data
# To JSON file
gh search repos "topic:cli" --json fullName,stars --limit 100 > repos.json
# To CSV (via jq)
gh pr list --json number,title,author \
--jq '.[] | [.number, .title, .author.login] | @csv' > prs.csv
# To TSV
gh issue list --json number,title \
--jq '.[] | [.number, .title] | @tsv' > issues.tsv---
Batch Operations
Iterating over results
# Process PRs
gh pr list --json number --jq '.[].number' | while read pr; do
echo "Processing PR #$pr"
gh pr view "$pr" --json title --jq '.title'
done
# Process repos from search
gh search code "path:.skilz" --json repository --jq '.[].repository.fullName' | \
sort -u | while read repo; do
echo "Found: $repo"
done
# Process with xargs (parallel)
gh pr list --label "auto-merge" --json number --jq '.[].number' | \
xargs -I {} -P 4 gh pr merge {} --squashBatch PR operations
# Merge all approved PRs
for pr in $(gh pr list --json number,reviewDecision \
--jq '.[] | select(.reviewDecision == "APPROVED") | .number'); do
gh pr merge "$pr" --squash --delete-branch
done
# Add label to multiple PRs
for pr in $(gh pr list --search "is:open author:dependabot" --json number --jq '.[].number'); do
gh pr edit "$pr" --add-label "dependencies"
done
# Close stale PRs
for pr in $(gh pr list --json number,updatedAt \
--jq --arg cutoff "$(date -d '30 days ago' -Iseconds)" \
'.[] | select(.updatedAt < $cutoff) | .number'); do
gh pr close "$pr" --comment "Closing stale PR"
doneBatch issue operations
# Assign all unassigned bugs
gh issue list --label bug --json number,assignees \
--jq '.[] | select(.assignees | length == 0) | .number' | \
while read num; do
gh issue edit "$num" --add-assignee @me
done
# Add to project
for issue in $(gh issue list --label "priority:high" --json number --jq '.[].number'); do
gh project item-add 1 --url "https://github.com/owner/repo/issues/$issue"
doneMulti-repo operations
# Clone matching repos
gh search repos --owner myorg --topic python --json fullName --jq '.[].fullName' | \
while read repo; do
gh repo clone "$repo" "clones/$(basename $repo)" || true
done
# Create issue across repos
REPOS=("org/repo1" "org/repo2" "org/repo3")
for repo in "${REPOS[@]}"; do
gh issue create --repo "$repo" \
--title "Update dependencies" \
--label "maintenance" \
--body "Please update dependencies"
done
# Check CI status across repos
gh search repos --owner myorg --json fullName --jq '.[].fullName' | \
while read repo; do
latest=$(gh run list --repo "$repo" --limit 1 --json conclusion --jq '.[0].conclusion' 2>/dev/null)
echo "$repo: ${latest:-no runs}"
done---
Error Handling
Exit code checking
#!/bin/bash
set -euo pipefail
# Check command success
if gh pr merge 123 --squash; then
echo "PR merged successfully"
else
echo "Failed to merge PR"
exit 1
fi
# Capture exit code
gh run watch 12345 --exit-status
EXIT_CODE=$?
case $EXIT_CODE in
0) echo "Success" ;;
1) echo "Failed" ;;
2) echo "Cancelled" ;;
*) echo "Unknown: $EXIT_CODE" ;;
esacError output handling
# Capture stderr
if ! OUTPUT=$(gh pr create --fill 2>&1); then
echo "Error: $OUTPUT"
exit 1
fi
# Suppress errors, continue
gh repo clone owner/repo 2>/dev/null || true
# Log errors to file
gh run view 12345 2>> errors.logValidation before operations
# Check if PR exists
if ! gh pr view 123 &>/dev/null; then
echo "PR #123 not found"
exit 1
fi
# Check if repo accessible
if ! gh repo view owner/repo &>/dev/null; then
echo "Cannot access repo"
exit 1
fi
# Check auth status
if ! gh auth status &>/dev/null; then
echo "Not authenticated"
exit 1
fiSafe operations pattern
#!/bin/bash
set -euo pipefail
safe_merge() {
local pr=$1
# Validate PR exists and is open
state=$(gh pr view "$pr" --json state --jq '.state')
if [ "$state" != "OPEN" ]; then
echo "PR #$pr is not open (state: $state)"
return 1
fi
# Check CI passed
if ! gh pr checks "$pr" --watch --fail-fast; then
echo "PR #$pr checks failed"
return 1
fi
# Merge
gh pr merge "$pr" --squash --delete-branch
}
safe_merge 123---
Retry Patterns
Simple retry
retry() {
local max_attempts=$1
shift
local attempt=1
until "$@"; do
if [ $attempt -ge $max_attempts ]; then
echo "Failed after $attempt attempts"
return 1
fi
echo "Attempt $attempt failed, retrying..."
((attempt++))
sleep 2
done
}
retry 3 gh api repos/owner/repoExponential backoff
retry_backoff() {
local max_attempts=${1:-5}
shift
local attempt=0
local delay=1
until "$@"; do
((attempt++))
if [ $attempt -ge $max_attempts ]; then
echo "Failed after $attempt attempts"
return 1
fi
echo "Attempt $attempt failed, waiting ${delay}s..."
sleep $delay
delay=$((delay * 2))
done
}
retry_backoff 5 gh workflow run deploy.ymlRetry with jitter
retry_jitter() {
local max_attempts=${1:-5}
shift
local attempt=0
local base_delay=1
until "$@"; do
((attempt++))
if [ $attempt -ge $max_attempts ]; then
return 1
fi
# Add random jitter (0-1000ms)
local jitter=$(( RANDOM % 1000 ))
local delay=$(( base_delay * attempt + jitter / 1000 ))
sleep "$delay"
done
}Retry specific errors
retry_on_rate_limit() {
local max_attempts=5
local attempt=0
while [ $attempt -lt $max_attempts ]; do
if output=$("$@" 2>&1); then
echo "$output"
return 0
fi
if echo "$output" | grep -q "rate limit"; then
((attempt++))
echo "Rate limited, waiting 60s (attempt $attempt/$max_attempts)"
sleep 60
else
echo "$output" >&2
return 1
fi
done
return 1
}
retry_on_rate_limit gh api search/code -f q='filename:SKILL.md'---
Rate Limit Handling
Checking limits
# Core API limits
gh api rate_limit --jq '.resources.core'
# Search API limits
gh api rate_limit --jq '.resources.search'
# Code search limits
gh api rate_limit --jq '.resources.code_search'
# All limits
gh api rate_limit --jq '.resources | to_entries | .[] | "\(.key): \(.value.remaining)/\(.value.limit)"'Respecting limits in scripts
#!/bin/bash
check_rate_limit() {
local resource=${1:-core}
local remaining=$(gh api rate_limit --jq ".resources.$resource.remaining")
local reset=$(gh api rate_limit --jq ".resources.$resource.reset")
if [ "$remaining" -lt 10 ]; then
local wait_time=$((reset - $(date +%s)))
if [ $wait_time -gt 0 ]; then
echo "Rate limit low ($remaining remaining), waiting ${wait_time}s"
sleep $wait_time
fi
fi
}
# Use before API-heavy operations
check_rate_limit search
gh search code "path:.skilz" --limit 100Caching for rate limits
# Cache API responses
gh api --cache 3600s repos/owner/repo
# Cache for 1 hour
gh api --cache 1h repos/owner/repo/releases
# Check if cached
gh api --cache 3600s repos/owner/repo 2>&1 | grep -q "cached"Throttling batch operations
# Add delay between operations
for repo in "${repos[@]}"; do
gh api "repos/$repo" --jq '.stargazers_count'
sleep 0.5 # 2 requests per second max
done
# Respect search limit (10/min for code)
for query in "${queries[@]}"; do
gh search code "$query" --limit 100
sleep 6 # Stay under 10/minute
done---
Environment Setup
Environment variables
| Variable | Purpose | Example |
|---|---|---|
GH_TOKEN | Authentication | export GH_TOKEN=ghp_xxx |
GH_REPO | Default repository | export GH_REPO=owner/repo |
GH_HOST | GitHub Enterprise | export GH_HOST=github.mycompany.com |
GH_PROMPT_DISABLED | Disable prompts | export GH_PROMPT_DISABLED=1 |
GH_DEBUG | Debug output | export GH_DEBUG=1 |
GH_PAGER | Pager command | export GH_PAGER=less |
NO_COLOR | Disable colors | export NO_COLOR=1 |
GitHub Actions setup
name: Automation
on: workflow_dispatch
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
jobs:
automate:
runs-on: ubuntu-latest
permissions:
contents: read
pull-requests: write
issues: write
steps:
- uses: actions/checkout@v4
- name: Run automation
run: |
gh pr list --json number,title
gh issue create --title "Auto" --body "Created by CI"Using PAT for cross-repo access
env:
GH_TOKEN: ${{ secrets.PAT_TOKEN }} # Personal Access Token
steps:
- name: Access other repos
run: |
gh repo clone other-org/other-repo
gh pr create --repo other-org/other-repo --fillScript header template
#!/bin/bash
set -euo pipefail
# Verify authentication
if ! gh auth status &>/dev/null; then
echo "Error: Not authenticated with GitHub CLI"
echo "Run: gh auth login"
exit 1
fi
# Set defaults
: "${GH_REPO:=owner/repo}"
export GH_PROMPT_DISABLED=1
# Your automation here---
Complete Workflow Examples
Search, analyze, and report
#!/bin/bash
set -euo pipefail
# Find all repos with .skilz directory
echo "Searching for repos with .skilz..."
repos=$(gh search code "path:.skilz" --json repository \
--jq '[.[].repository.fullName] | unique | .[]')
echo "Found $(echo "$repos" | wc -l) repos"
# Analyze each repo
for repo in $repos; do
echo "=== $repo ==="
# Get repo info
info=$(gh api "repos/$repo" --jq '{stars: .stargazers_count, language: .language}')
echo " Info: $info"
# Check latest CI status
latest=$(gh run list --repo "$repo" --limit 1 --json conclusion --jq '.[0].conclusion' 2>/dev/null || echo "none")
echo " CI: $latest"
sleep 1 # Rate limit courtesy
doneAuto-merge approved PRs
#!/bin/bash
set -euo pipefail
echo "Finding approved PRs..."
approved=$(gh pr list --json number,reviewDecision,statusCheckRollup \
--jq '[.[] | select(.reviewDecision == "APPROVED")] | .[].number')
for pr in $approved; do
echo "Processing PR #$pr..."
# Wait for checks
if gh pr checks "$pr" --watch --fail-fast; then
echo " Checks passed, merging..."
gh pr merge "$pr" --squash --delete-branch
echo " Merged!"
else
echo " Checks failed, skipping"
fi
doneTrigger deploy and monitor
#!/bin/bash
set -euo pipefail
ENVIRONMENT=${1:-staging}
TIMEOUT=${2:-3600}
echo "Triggering deploy to $ENVIRONMENT..."
gh workflow run deploy.yml -f environment="$ENVIRONMENT"
# Wait for run to register
sleep 5
# Get run ID
RUN_ID=$(gh run list --workflow=deploy.yml --limit 1 --json databaseId --jq '.[0].databaseId')
echo "Run ID: $RUN_ID"
# Watch with timeout
echo "Watching run (timeout: ${TIMEOUT}s)..."
if timeout "$TIMEOUT" gh run watch "$RUN_ID" --exit-status; then
echo "Deploy succeeded!"
# Download artifacts
gh run download "$RUN_ID" -D ./deploy-artifacts
exit 0
else
echo "Deploy failed!"
# Show failed logs
gh run view "$RUN_ID" --log-failed
exit 1
fiSync forks and create update PRs
#!/bin/bash
set -euo pipefail
# List of forks to sync
FORKS=(
"myorg/forked-repo-1"
"myorg/forked-repo-2"
)
for fork in "${FORKS[@]}"; do
echo "=== Syncing $fork ==="
# Sync with upstream
if gh repo sync "$fork" --force; then
echo " Synced successfully"
else
echo " Sync failed, skipping"
continue
fi
# Clone and check for updates needed
tmpdir=$(mktemp -d)
gh repo clone "$fork" "$tmpdir" -- --depth 1
# ... make updates ...
rm -rf "$tmpdir"
doneComprehensive repo audit
#!/bin/bash
set -euo pipefail
ORG=${1:-myorg}
OUTPUT=${2:-audit.json}
echo "Auditing $ORG repositories..."
gh search repos --owner "$ORG" --limit 1000 --json fullName,visibility,isArchived \
--jq '.[] | select(.isArchived == false)' | \
while read -r repo_json; do
repo=$(echo "$repo_json" | jq -r '.fullName')
# Get additional details
details=$(gh api "repos/$repo" --jq '{
has_issues: .has_issues,
has_wiki: .has_wiki,
default_branch: .default_branch,
pushed_at: .pushed_at
}')
# Get branch protection
protection=$(gh api "repos/$repo/branches/main/protection" 2>/dev/null || echo '{"enabled": false}')
# Combine and output
echo "$repo_json" | jq --argjson details "$details" --argjson protection "$protection" \
'. + $details + {branch_protection: $protection}'
sleep 0.5 # Rate limit
done | jq -s '.' > "$OUTPUT"
echo "Audit complete: $OUTPUT"CI/CD Monitoring Reference
Comprehensive reference for monitoring GitHub Actions workflows, PR checks, and retrieving logs/artifacts.
Contents
- Workflow Runs
- PR Checks
- Logs & Artifacts
- Watching & Waiting
- JSON Fields Reference
- Exit Codes
- Common Recipes
---
Workflow Runs
Listing runs
gh run list [flags]| Flag | Description | Example |
|---|---|---|
--workflow | Filter by workflow name/file | --workflow=CI |
--branch | Filter by branch | --branch=main |
--status | Filter by status | --status=failure |
--event | Filter by trigger event | --event=push |
--user | Filter by user who triggered | --user=octocat |
--limit | Max results (default 20) | --limit=50 |
--json | JSON output | --json databaseId,status |
Status values
| Status | Meaning |
|---|---|
queued | Waiting to run |
in_progress | Currently running |
completed | Finished (check conclusion) |
waiting | Waiting for approval |
pending | Not yet started |
requested | Workflow requested |
Conclusion values (when completed)
| Conclusion | Meaning |
|---|---|
success | All jobs passed |
failure | One or more jobs failed |
cancelled | Run was cancelled |
skipped | Run was skipped |
timed_out | Run exceeded time limit |
action_required | Needs manual approval |
neutral | Neutral result |
stale | Outdated run |
Examples
# Recent failures on main
gh run list --workflow=CI --branch=main --status=failure --limit 10
# All runs by user
gh run list --user=octocat --limit 20
# JSON for automation
gh run list --json databaseId,status,conclusion,workflowName --limit 50
# Filter completed runs
gh run list --json databaseId,conclusion \
--jq '.[] | select(.conclusion == "failure")'Viewing run details
gh run view <run-id> [flags]| Flag | Description |
|---|---|
--log | Show full logs |
--log-failed | Show only failed step logs |
--verbose | Show job steps |
--exit-status | Exit with run's status code |
--json | JSON output |
--web | Open in browser |
Examples
# Summary view
gh run view 12345
# With job details
gh run view 12345 --verbose
# Full logs
gh run view 12345 --log
# Only failed logs (most useful for debugging)
gh run view 12345 --log-failed
# JSON output
gh run view 12345 --json jobs,status,conclusion---
PR Checks
Checking PR status
gh pr checks [pr-number] [flags]| Flag | Description |
|---|---|
--watch | Block until checks complete |
--fail-fast | Exit on first failure (with --watch) |
--required | Show only required checks |
--json | JSON output |
--interval | Polling interval (default 10s) |
Examples
# View current PR checks
gh pr checks
# Specific PR
gh pr checks 123
# Block until complete
gh pr checks --watch
# Exit immediately on failure
gh pr checks --watch --fail-fast
# Only required checks
gh pr checks --required
# JSON output
gh pr checks --json name,state,bucket,completedAtPR status overview
gh pr status [flags]Shows overview of:
- PRs created by you
- PRs requesting your review
- PRs on current branch
# Basic status
gh pr status
# Include merge conflict info
gh pr status --conflict-status
# JSON output
gh pr status --jsonPR view for CI details
# Get check rollup status
gh pr view 123 --json statusCheckRollup
# Get review decision
gh pr view 123 --json reviewDecision
# Combined status info
gh pr view 123 --json statusCheckRollup,reviewDecision,mergeable---
Logs & Artifacts
Viewing logs
# Full logs for run
gh run view 12345 --log
# Only failed step logs
gh run view 12345 --log-failed
# Specific job logs via API
gh api repos/{owner}/{repo}/actions/jobs/{job_id}/logsDownloading logs via API
# Download as zip
gh api repos/{owner}/{repo}/actions/runs/12345/logs > logs.zip
# Extract and search
unzip -p logs.zip | grep -i "error"Downloading artifacts
gh run download <run-id> [flags]| Flag | Description | Example |
|---|---|---|
-n | Artifact name | -n build-output |
-p | Name pattern | -p "coverage-*" |
-D | Output directory | -D ./artifacts |
Examples
# Download all artifacts
gh run download 12345
# Specific artifact
gh run download 12345 -n build-output
# Pattern matching
gh run download 12345 -p "test-results-*"
# To specific directory
gh run download 12345 -n dist -D ./release
# Most recent run's artifacts
gh run download $(gh run list --limit 1 --json databaseId --jq '.[0].databaseId')Artifact management via API
# List artifacts for run
gh api repos/{owner}/{repo}/actions/runs/12345/artifacts
# List all repo artifacts
gh api repos/{owner}/{repo}/actions/artifacts
# Download specific artifact
gh api repos/{owner}/{repo}/actions/artifacts/{artifact_id}/zip > artifact.zip
# Delete artifact
gh api -X DELETE repos/{owner}/{repo}/actions/artifacts/{artifact_id}---
Watching & Waiting
Blocking watch
gh run watch <run-id> [flags]| Flag | Description |
|---|---|
--exit-status | Exit with run's conclusion code |
--interval | Polling interval (default 3s) |
Exit codes for watch
| Exit Code | Meaning |
|---|---|
0 | Run succeeded |
1 | Run failed or error |
2 | Run cancelled |
Examples
# Watch and exit with status
gh run watch 12345 --exit-status
# Custom interval
gh run watch 12345 --interval 10
# Watch most recent run
gh run watch $(gh run list --limit 1 --json databaseId --jq '.[0].databaseId') --exit-statusTrigger and watch pattern
# Trigger workflow and wait for completion
gh workflow run deploy.yml -f environment=staging
sleep 5 # Allow run to register
RUN_ID=$(gh run list --workflow=deploy.yml --limit 1 --json databaseId --jq '.[0].databaseId')
gh run watch "$RUN_ID" --exit-statusPR checks watch
# Block until PR checks complete
gh pr checks --watch
# Exit on first failure
gh pr checks --watch --fail-fast
# With custom interval
gh pr checks --watch --interval 30Timeout handling
For timeout control, use the script scripts/wait-for-run.sh:
./scripts/wait-for-run.sh 12345 3600 # 1 hour timeoutOr with shell timeout:
timeout 3600 gh run watch 12345 --exit-status---
JSON Fields Reference
gh run list fields
| Field | Type | Description |
|---|---|---|
databaseId | number | Run ID |
status | string | Current status |
conclusion | string | Final result |
workflowName | string | Workflow name |
workflowDatabaseId | number | Workflow ID |
headBranch | string | Branch name |
headSha | string | Commit SHA |
event | string | Trigger event |
displayTitle | string | Run title |
createdAt | timestamp | Start time |
updatedAt | timestamp | Last update |
url | string | Web URL |
gh run view --json jobs fields
| Field | Type | Description |
|---|---|---|
jobs | array | Job details |
jobs[].name | string | Job name |
jobs[].status | string | Job status |
jobs[].conclusion | string | Job result |
jobs[].startedAt | timestamp | Start time |
jobs[].completedAt | timestamp | End time |
jobs[].steps | array | Step details |
gh pr checks --json fields
| Field | Type | Description |
|---|---|---|
name | string | Check name |
state | string | Check state |
bucket | string | pass/fail/pending |
completedAt | timestamp | Completion time |
detailsUrl | string | Link to details |
workflowName | string | Workflow name |
---
Exit Codes
gh run watch
| Code | Meaning |
|---|---|
0 | success |
1 | failure or error |
2 | cancelled |
gh pr checks
| Code | Meaning |
|---|---|
0 | All checks passed |
1 | Error occurred |
8 | Checks pending/in progress |
Using exit codes in scripts
#!/bin/bash
set -e
if gh run watch 12345 --exit-status; then
echo "Run succeeded"
gh run download 12345 -n artifacts
else
echo "Run failed"
gh run view 12345 --log-failed
exit 1
fi#!/bin/bash
gh pr checks --watch
EXIT_CODE=$?
case $EXIT_CODE in
0) echo "All checks passed" ;;
1) echo "Error occurred" ;;
8) echo "Checks still pending" ;;
esac---
Common Recipes
Wait for latest run on branch
RUN_ID=$(gh run list --branch=main --limit 1 --json databaseId --jq '.[0].databaseId')
gh run watch "$RUN_ID" --exit-statusGet failed runs from last 24 hours
gh run list --status=failure --json databaseId,workflowName,createdAt --limit 100 | \
jq --arg cutoff "$(date -u -d '24 hours ago' +%Y-%m-%dT%H:%M:%SZ)" \
'.[] | select(.createdAt > $cutoff)'Download artifacts from successful runs only
for run_id in $(gh run list --status=completed --json databaseId,conclusion \
--jq '.[] | select(.conclusion=="success") | .databaseId'); do
gh run download "$run_id" -D "./artifacts/$run_id" 2>/dev/null || true
doneMonitor multiple workflows
WORKFLOWS=("ci.yml" "deploy.yml" "test.yml")
for wf in "${WORKFLOWS[@]}"; do
echo "=== $wf ==="
gh run list --workflow="$wf" --limit 5 --json status,conclusion,createdAt \
--jq '.[] | "\(.status) \(.conclusion // "pending") \(.createdAt)"'
doneAlert on PR check failure
#!/bin/bash
PR_NUM=${1:-$(gh pr view --json number --jq '.number')}
if ! gh pr checks "$PR_NUM" --watch --fail-fast; then
echo "PR #$PR_NUM checks failed!"
gh pr checks "$PR_NUM" --json name,state,bucket \
--jq '.[] | select(.bucket != "pass") | "\(.name): \(.state)"'
exit 1
fi
echo "PR #$PR_NUM all checks passed"Get workflow run duration
gh run view 12345 --json createdAt,updatedAt \
--jq '((.updatedAt | fromdateiso8601) - (.createdAt | fromdateiso8601)) | "\(. / 60 | floor)m \(. % 60)s"'Rerun failed and watch
gh run rerun 12345 --failed
sleep 3
NEW_RUN=$(gh run list --limit 1 --json databaseId --jq '.[0].databaseId')
gh run watch "$NEW_RUN" --exit-statusResource Creation Reference
Comprehensive reference for creating and managing PRs, issues, repositories, labels, and milestones.
Contents
---
Pull Requests
Creating PRs
gh pr create [flags]| Flag | Description | Example |
|---|---|---|
--title | PR title | --title "Add feature" |
--body | PR description | --body "Description" |
--body-file | Read body from file | --body-file PR.md |
--base | Target branch | --base main |
--head | Source branch | --head feature |
--reviewer | Request reviewers | --reviewer @user,team |
--assignee | Assign users | --assignee @me |
--label | Add labels | --label "bug,urgent" |
--milestone | Set milestone | --milestone "v1.0" |
--project | Add to project | --project "Roadmap" |
--draft | Create as draft | --draft |
--fill | Use commit info | --fill |
--fill-first | Use first commit | --fill-first |
--web | Open in browser | --web |
--no-maintainer-edit | Disable maintainer edits | --no-maintainer-edit |
Examples
# Basic PR
gh pr create --title "Fix bug" --body "Fixes issue #123"
# From commit messages
gh pr create --fill
# Full options
gh pr create \
--title "Add authentication" \
--body-file .github/PULL_REQUEST_TEMPLATE.md \
--base develop \
--head feature/auth \
--reviewer security-team,@octocat \
--assignee @me \
--label "feature,security" \
--milestone "v2.0" \
--draft
# Cross-repo PR (fork to upstream)
gh pr create --repo upstream/repo --head myuser:feature-branchManaging PRs
# List PRs
gh pr list
gh pr list --state open --assignee @me
gh pr list --label "needs-review" --json number,title
# View PR
gh pr view 123
gh pr view 123 --json title,body,reviews
# Edit PR
gh pr edit 123 --title "New title"
gh pr edit 123 --add-label "approved" --remove-label "needs-review"
gh pr edit 123 --add-reviewer @user
# Review PR
gh pr review 123 --approve
gh pr review 123 --request-changes --body "Please fix X"
gh pr review 123 --comment --body "Looks good overall"
# Merge PR
gh pr merge 123
gh pr merge 123 --squash
gh pr merge 123 --rebase
gh pr merge 123 --auto --squash # Enable auto-merge
gh pr merge 123 --delete-branch
# Close without merging
gh pr close 123
gh pr close 123 --comment "Closing: superseded by #456"
# Reopen
gh pr reopen 123PR checkout and diff
# Checkout PR locally
gh pr checkout 123
# View diff
gh pr diff 123
gh pr diff 123 --patch > changes.patch---
Issues
Creating issues
gh issue create [flags]| Flag | Description | Example |
|---|---|---|
--title | Issue title | --title "Bug report" |
--body | Issue description | --body "Details..." |
--body-file | Read body from file | --body-file issue.md |
--label | Add labels | --label "bug,urgent" |
--assignee | Assign users | --assignee @me,@user |
--milestone | Set milestone | --milestone "v1.0" |
--project | Add to project | --project "Backlog" |
--web | Open in browser | --web |
Examples
# Basic issue
gh issue create --title "Bug: Login fails" --body "Steps to reproduce..."
# With labels and assignee
gh issue create \
--title "Feature request" \
--body "Please add..." \
--label "enhancement,priority:high" \
--assignee @me
# From file
gh issue create --title "Release checklist" --body-file checklist.md
# From stdin (automation)
echo "Automated issue body" | gh issue create --title "Auto-created" --body-file -
# Using template
gh issue create --template bug_report.mdManaging issues
# List issues
gh issue list
gh issue list --state open --assignee @me
gh issue list --label "bug" --limit 50
gh issue list --json number,title,labels
# View issue
gh issue view 123
gh issue view 123 --json title,body,comments
# Edit issue
gh issue edit 123 --title "Updated title"
gh issue edit 123 --add-label "in-progress" --remove-label "triage"
gh issue edit 123 --add-assignee @user
# Close issue
gh issue close 123
gh issue close 123 --comment "Fixed in #456"
gh issue close 123 --reason "not planned"
# Reopen issue
gh issue reopen 123
# Transfer issue
gh issue transfer 123 target-owner/target-repo
# Pin/unpin
gh issue pin 123
gh issue unpin 123
# Delete (requires confirmation)
gh issue delete 123 --yesIssue comments
# Add comment
gh issue comment 123 --body "Working on this"
# From file
gh issue comment 123 --body-file update.md
# Edit comment (via API)
gh api repos/{owner}/{repo}/issues/comments/{comment_id} \
-X PATCH -f body="Updated comment"---
Repositories
Creating repositories
gh repo create [name] [flags]| Flag | Description | Example |
|---|---|---|
--public | Public visibility | --public |
--private | Private visibility | --private |
--internal | Internal (enterprise) | --internal |
--clone | Clone after creating | --clone |
--description | Repo description | --description "My project" |
--homepage | Homepage URL | --homepage "https://..." |
--license | License template | --license mit |
--gitignore | Gitignore template | --gitignore Python |
--template | Template repository | --template owner/template |
--add-readme | Initialize with README | --add-readme |
--disable-issues | Disable issues | --disable-issues |
--disable-wiki | Disable wiki | --disable-wiki |
Examples
# Public repo with defaults
gh repo create my-project --public --clone
# Full options
gh repo create my-app \
--public \
--clone \
--description "My awesome app" \
--license mit \
--gitignore Node \
--add-readme
# From template
gh repo create my-project --template org/template-repo --private --clone
# In organization
gh repo create myorg/new-project --private
# Interactive mode
gh repo createForking repositories
gh repo fork [repo] [flags]| Flag | Description | Example |
|---|---|---|
--clone | Clone after forking | --clone |
--remote | Add remote | --remote |
--remote-name | Remote name | --remote-name upstream |
--org | Fork to organization | --org myorg |
--fork-name | Custom fork name | --fork-name my-fork |
Examples
# Fork and clone
gh repo fork owner/repo --clone
# Fork to org
gh repo fork owner/repo --clone --org my-org
# Add upstream remote
gh repo fork owner/repo --clone --remote-name upstreamSyncing forks
# Sync with upstream
gh repo sync
# Sync specific branch
gh repo sync --branch main
# Force sync (discard local changes)
gh repo sync --force
# Sync specific fork
gh repo sync owner/fork --source upstream/repoCloning repositories
# Clone
gh repo clone owner/repo
# Clone to specific directory
gh repo clone owner/repo my-directory
# Clone with git options
gh repo clone owner/repo -- --depth 1
# Clone specific branch
gh repo clone owner/repo -- -b developRepository management
# View repo
gh repo view
gh repo view owner/repo
gh repo view --json name,description,stargazerCount
# Edit repo
gh repo edit --description "Updated description"
gh repo edit --visibility private
gh repo edit --enable-issues --enable-wiki
# Archive/unarchive
gh repo archive owner/repo
gh repo unarchive owner/repo
# Delete (requires confirmation)
gh repo delete owner/repo --yes
# Rename
gh repo rename new-name---
Labels
Managing labels
# List labels
gh label list
gh label list --json name,color,description
# Create label
gh label create "priority:high" --color FF0000 --description "High priority"
gh label create "bug" --color d73a4a
# Edit label
gh label edit "bug" --name "bug-fix" --color 00FF00
# Delete label
gh label delete "old-label" --yes
# Clone labels from another repo
gh label clone source-owner/source-repo
gh label clone source-owner/source-repo --force # Overwrite existingLabel colors (common)
| Color | Hex |
|---|---|
| Red | d73a4a |
| Orange | f9a825 |
| Yellow | fbca04 |
| Green | 0e8a16 |
| Blue | 1d76db |
| Purple | 5319e7 |
| Gray | 7f8c8d |
---
Milestones
Milestones require API access:
# List milestones
gh api repos/{owner}/{repo}/milestones
# Create milestone
gh api repos/{owner}/{repo}/milestones \
-f title="v1.0" \
-f description="First release" \
-f due_on="2025-12-31T23:59:59Z" \
-f state="open"
# Update milestone
gh api repos/{owner}/{repo}/milestones/{number} \
-X PATCH \
-f description="Updated description"
# Close milestone
gh api repos/{owner}/{repo}/milestones/{number} \
-X PATCH \
-f state="closed"
# Delete milestone
gh api repos/{owner}/{repo}/milestones/{number} -X DELETE---
Projects
Managing projects
# List projects
gh project list
gh project list --owner myorg
# Create project
gh project create --title "Q1 Roadmap"
gh project create --title "Sprint 1" --owner myorg
# View project
gh project view 1
gh project view 1 --json title,items
# Add item to project
gh project item-add 1 --url https://github.com/owner/repo/issues/123
# List items
gh project item-list 1
gh project item-list 1 --json title,status
# Edit item
gh project item-edit --project-id 1 --id ITEM_ID --field-id FIELD_ID --text "Done"
# Delete project
gh project delete 1
# Close project
gh project close 1---
Releases
Managing releases
# List releases
gh release list
gh release list --limit 10
# Create release
gh release create v1.0.0
gh release create v1.0.0 --title "Version 1.0.0" --notes "Release notes"
gh release create v1.0.0 --notes-file CHANGELOG.md
gh release create v1.0.0 --draft
gh release create v1.0.0 --prerelease
gh release create v1.0.0 ./dist/* # Upload assets
# View release
gh release view v1.0.0
gh release view v1.0.0 --json tagName,assets
# Edit release
gh release edit v1.0.0 --title "Updated title"
gh release edit v1.0.0 --draft=false # Publish draft
# Download assets
gh release download v1.0.0
gh release download v1.0.0 -p "*.tar.gz" -D ./downloads
# Upload assets
gh release upload v1.0.0 ./dist/app.zip
# Delete release
gh release delete v1.0.0 --yes
gh release delete v1.0.0 --cleanup-tag # Also delete tag---
Common Recipes
Create PR and auto-merge when ready
gh pr create --fill
gh pr merge --auto --squashCreate issue from template and assign
gh issue create \
--title "Weekly sync" \
--body-file .github/ISSUE_TEMPLATE/meeting.md \
--label "meeting" \
--assignee @me,@teammateBatch create issues
while IFS=, read -r title label; do
gh issue create --title "$title" --label "$label" --body "Auto-generated"
done < issues.csvFork, clone, create branch, PR
gh repo fork upstream/repo --clone
cd repo
git checkout -b my-feature
# ... make changes ...
git add . && git commit -m "Add feature"
git push -u origin my-feature
gh pr create --fill --repo upstream/repoSync fork and update PR
gh repo sync --branch main
git checkout my-feature
git rebase main
git push --force-with-leaseClone all repos matching criteria
gh search repos --owner myorg --language python --json name --jq '.[].name' | \
while read repo; do
gh repo clone "myorg/$repo" "repos/$repo"
doneCreate release with changelog
# Generate changelog from commits
git log v0.9.0..HEAD --pretty=format:"- %s" > /tmp/notes.md
# Create release with assets
gh release create v1.0.0 \
--title "Version 1.0.0" \
--notes-file /tmp/notes.md \
./dist/*.tar.gz ./dist/*.zipTransfer issues between repos
# List and transfer all open bugs
gh issue list --label "bug" --json number --jq '.[].number' | \
while read num; do
gh issue transfer "$num" target-org/target-repo
doneSearch & Discovery Reference
Comprehensive reference for finding repositories and code on GitHub using gh CLI.
Contents
- Repository Search
- Code Search
- Path-Based Discovery
- Search Qualifiers
- Handling Large Results
- API Search Patterns
- Common Recipes
---
Repository Search
Basic syntax
gh search repos [query] [flags]Filter flags
| Flag | Description | Example |
|---|---|---|
--language | Filter by language | --language=python |
--owner | Filter by owner/org | --owner=microsoft |
--topic | Filter by topic | --topic=cli |
--stars | Filter by star count | --stars=">100" |
--forks | Filter by fork count | --forks=">=50" |
--created | Filter by creation date | --created=">2024-01-01" |
--pushed | Filter by last push | --pushed=">=2024-06-01" |
--archived | Include/exclude archived | --archived=false |
--visibility | public/private/internal | --visibility=public |
--license | Filter by license | --license=mit |
--limit | Max results (default 30) | --limit=100 |
--sort | Sort field | --sort=stars |
--order | Sort order | --order=desc |
Numeric ranges
# Exact value
--stars=100
# Greater/less than
--stars=">100"
--stars="<50"
--stars=">=100"
# Range
--stars="100..1000"
# Unlimited upper bound
--stars="100..*"Date ranges
# After date
--created=">2024-01-01"
# Before date
--pushed="<2024-06-01"
# Range
--created="2024-01-01..2024-06-30"Examples
# Popular Python CLIs
gh search repos --language=python --topic=cli --stars=">100" --archived=false
# Recently active in org
gh search repos --owner=myorg --pushed=">=2024-01-01"
# With specific license
gh search repos "database" --language=go --license=apache-2.0
# JSON output
gh search repos "machine learning" --json fullName,stargazersCount,url --limit 50---
Code Search
Basic syntax
gh search code [query] [flags]Filter flags
| Flag | Description | Example |
|---|---|---|
--filename | Match filename | --filename=Dockerfile |
--extension | Match extension | --extension=py |
--language | Filter by language | --language=python |
--repo | Search specific repo | --repo=owner/name |
--owner | Search owner/org | --owner=myorg |
--limit | Max results | --limit=100 |
Query qualifiers (in query string)
| Qualifier | Description | Example |
|---|---|---|
path: | Match file path | "path:src/components" |
path:/ | Match repo root | "path:/ README" |
filename: | Match filename | "filename:config.yml" |
extension: | Match extension | "extension:ts" |
language: | Filter language | "language:javascript" |
repo: | Specific repo | "repo:owner/name" |
org: | Organization | "org:github" |
user: | User repos | "user:octocat" |
size: | File size (bytes) | "size:>1000" |
Examples
# Find specific files
gh search code --filename SKILL.md
gh search code --filename Dockerfile --owner myorg
gh search code --filename pyproject.toml --language python
# Search file contents
gh search code "TODO" --extension py --owner myorg
gh search code "import pandas" --language python
# Combined qualifiers
gh search code "path:.github" --filename workflow.yml
gh search code "config" --extension json --repo owner/repo---
Path-Based Discovery
Find repositories containing specific directory structures.
Directory detection patterns
# Root-level directories
gh search code "path:.skilz"
gh search code "path:.cursor"
gh search code "path:.codex"
gh search code "path:.github"
# Nested paths
gh search code "path:src/components"
gh search code "path:packages/core"
gh search code "path:some/nested/.skilz"
# Any file in directory
gh search code "path:.skilz" --json repository --jq '.[].repository.fullName'Finding repos with specific structures
# Repos with .skilz directory
gh search code "path:.skilz" --json repository --jq '[.[].repository.fullName] | unique'
# Repos with .cursor config
gh search code "path:.cursor" --owner myorg --json repository,path
# Repos with specific nested structure
gh search code "path:config/settings" --json repository --jq '[.[].repository.fullName] | unique'Combining path with filename
# SKILL.md in .skilz directory
gh search code "path:.skilz" --filename SKILL.md
# workflow.yml in .github/workflows
gh search code "path:.github/workflows" --filename "*.yml"
# Config files in specific paths
gh search code "path:src/config" --extension jsonExtracting unique repos
# Get unique repo names from code search
gh search code "path:.skilz" --json repository --jq '[.[].repository.fullName] | unique | .[]'
# With owner filter
gh search code "path:.cursor" --owner myorg --json repository \
--jq '[.[].repository.fullName] | unique | .[]'
# Count repos
gh search code "path:.codex" --json repository \
--jq '[.[].repository.fullName] | unique | length'---
Search Qualifiers
Code search qualifiers reference
| Qualifier | Operators | Example |
|---|---|---|
filename: | exact match | filename:Dockerfile |
path: | prefix match | path:src/ |
extension: | exact match | extension:py |
language: | exact match | language:python |
repo: | exact match | repo:owner/name |
org: | exact match | org:github |
user: | exact match | user:octocat |
size: | >, <, .. | size:>10000 |
Repository search qualifiers reference
| Qualifier | Operators | Example |
|---|---|---|
language: | exact match | language:rust |
stars: | >, <, .. | stars:>1000 |
forks: | >, <, .. | forks:>=100 |
created: | >, <, .. | created:>2024-01-01 |
pushed: | >, <, .. | pushed:>=2024-06-01 |
archived: | true, false | archived:false |
is: | public, private | is:public |
topic: | exact match | topic:cli |
license: | SPDX identifier | license:mit |
Using raw query syntax
For complex queries, use -- to pass raw query:
# Multiple exclusions
gh search repos -- "cli language:rust stars:>50 -topic:deprecated"
# NOT operator
gh search repos -- "-topic:linux" --language=python
# OR operator (in query)
gh search code -- "filename:Dockerfile OR filename:docker-compose.yml"---
Handling Large Results
GitHub Search API limits results to 1,000 maximum. Workarounds:
Date partitioning
Split searches by date ranges:
# First half of year
gh search repos --language=python --created="2024-01-01..2024-06-30" --limit 1000
# Second half of year
gh search repos --language=python --created="2024-07-01..2024-12-31" --limit 1000Star partitioning
Split by popularity:
# High stars
gh search repos --topic=cli --stars=">1000" --limit 1000
# Medium stars
gh search repos --topic=cli --stars="100..1000" --limit 1000
# Lower stars
gh search repos --topic=cli --stars="10..100" --limit 1000Automated partitioning script
Use scripts/batch-search.sh for automatic date partitioning:
./scripts/batch-search.sh "language:python topic:cli" 2024-01-01 2024-12-31Pagination limitations
Important: gh search commands do NOT support pagination flags. The --paginate flag only works with gh api.
# This does NOT work:
gh search repos --paginate # ERROR
# Use gh api instead for pagination:
gh api --paginate search/repositories -f q='language:python stars:>100'---
API Search Patterns
REST API search
# Repository search
gh api -X GET search/repositories -f q='language:python stars:>100'
# Code search
gh api -X GET search/code -f q='filename:SKILL.md path:.skilz'
# Issue/PR search
gh api -X GET search/issues -f q='repo:owner/name is:pr is:open'With jq filtering
# Extract repo names
gh api search/repositories -f q='topic:cli' \
--jq '.items[] | {name: .full_name, stars: .stargazers_count}'
# Get total count
gh api search/repositories -f q='language:rust' --jq '.total_count'
# Extract URLs
gh api search/code -f q='filename:Dockerfile' \
--jq '.items[] | .repository.html_url' | sort -uGraphQL search
gh api graphql -f query='
query($q: String!) {
search(query: $q, type: REPOSITORY, first: 100) {
repositoryCount
nodes {
... on Repository {
nameWithOwner
stargazerCount
description
}
}
}
}
' -f q='language:go stars:>500'Paginated API search
# REST with pagination
gh api --paginate search/repositories -f q='topic:cli' \
--jq '.items[].full_name'
# Note: Still limited to 1000 total results by GitHub---
Common Recipes
Find all repos with a specific file
gh search code --filename SKILL.md --json repository \
--jq '[.[].repository.fullName] | unique | .[]'Find repos with directory structure in org
gh search code "path:.skilz" --owner myorg --json repository,path \
--jq 'group_by(.repository.fullName) | map({repo: .[0].repository.fullName, files: map(.path)})'Search and clone matching repos
gh search code "path:.cursor" --json repository --jq '.[].repository.fullName' | \
sort -u | while read repo; do
gh repo clone "$repo" "repos/$(basename $repo)"
doneFind repos matching multiple criteria
# Has both Dockerfile and pyproject.toml
gh search code --filename Dockerfile --json repository --jq '.[].repository.fullName' | sort -u > /tmp/docker.txt
gh search code --filename pyproject.toml --json repository --jq '.[].repository.fullName' | sort -u > /tmp/python.txt
comm -12 /tmp/docker.txt /tmp/python.txtExport search results to JSON
gh search repos --language=python --stars=">100" --limit 100 \
--json fullName,description,stargazersCount,url \
> search-results.jsonCount repos by language in results
gh search repos "database" --limit 100 --json primaryLanguage \
--jq 'group_by(.primaryLanguage.name) | map({lang: .[0].primaryLanguage.name, count: length}) | sort_by(-.count)'---
Rate Limits
| Endpoint | Limit |
|---|---|
| Search API | 30 requests/minute |
| Code Search | 10 requests/minute |
| Max results | 1,000 per query |
Check limits
gh api rate_limit --jq '.resources.search'Handling rate limits
# Add delay between searches
for query in "${queries[@]}"; do
gh search code "$query" --json repository
sleep 6 # Stay under 10/minute for code search
doneGitHub Actions Workflow Authoring Reference
Comprehensive reference for writing GitHub Actions workflow YAML files.
Contents
- Workflow Structure
- Event Triggers
- Caching Strategies
- Matrix Builds
- OIDC Integration
- Reusable Workflows
- Composite Actions
- Security Best Practices
- Common Patterns
- Expression Reference
- Production Templates
- Advanced Patterns
- Workflow Summaries
- Container Security
- Security Scanning
- Troubleshooting Guide
- Performance Reference
- Workflow Checklist
---
Workflow Structure
Basic template
name: CI Pipeline
on:
push:
branches: [main, develop]
pull_request:
branches: [main]
workflow_dispatch:
jobs:
build:
runs-on: ubuntu-latest
timeout-minutes: 15
steps:
- uses: actions/checkout@v4
- name: Build
run: make build
- name: Test
run: make testJob dependencies
jobs:
build:
runs-on: ubuntu-latest
steps:
- run: make build
test:
needs: build
runs-on: ubuntu-latest
steps:
- run: make test
deploy:
needs: [build, test]
runs-on: ubuntu-latest
steps:
- run: make deployConcurrency control
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true---
Event Triggers
Common triggers
on:
# Push to branches
push:
branches: [main, develop]
paths:
- 'src/**'
- 'tests/**'
paths-ignore:
- 'docs/**'
# Pull requests
pull_request:
types: [opened, synchronize, reopened]
# Manual trigger
workflow_dispatch:
inputs:
environment:
description: 'Environment to deploy'
required: true
type: choice
options:
- staging
- production
# Scheduled
schedule:
- cron: '0 2 * * *' # 2 AM daily
# Reusable workflow
workflow_call:
inputs:
config:
required: true
type: string---
Caching Strategies
Python/Poetry
steps:
- uses: actions/setup-python@v5
with:
python-version: '3.11'
id: setup_python
- uses: actions/cache@v4
with:
path: |
~/.cache/pypoetry
.venv
key: poetry-${{ runner.os }}-${{ steps.setup_python.outputs.python-version }}-${{ hashFiles('poetry.lock') }}
restore-keys: |
poetry-${{ runner.os }}-${{ steps.setup_python.outputs.python-version }}-
- uses: snok/install-poetry@v1
with:
virtualenvs-in-project: true
- run: poetry install --no-interactionNode.js (npm/pnpm/yarn)
# npm (built-in caching)
- uses: actions/setup-node@v4
with:
node-version: '20'
cache: 'npm'
- run: npm ci
# pnpm
- uses: pnpm/action-setup@v2
with:
version: 8
- uses: actions/setup-node@v4
with:
node-version: '20'
cache: 'pnpm'
- run: pnpm install --frozen-lockfile
# yarn
- uses: actions/setup-node@v4
with:
node-version: '20'
cache: 'yarn'
- run: yarn install --frozen-lockfileJava (Maven/Gradle)
# Maven
- uses: actions/setup-java@v4
with:
java-version: '17'
distribution: 'temurin'
cache: 'maven'
- run: mvn clean install
# Gradle
- uses: actions/setup-java@v4
with:
java-version: '17'
distribution: 'temurin'
cache: 'gradle'
- run: ./gradlew buildDocker layer caching
- uses: docker/setup-buildx-action@v3
- uses: docker/build-push-action@v5
with:
context: .
cache-from: type=gha
cache-to: type=gha,mode=max---
Matrix Builds
Basic matrix
strategy:
matrix:
os: [ubuntu-latest, windows-latest, macos-latest]
python-version: ['3.9', '3.10', '3.11']
runs-on: ${{ matrix.os }}
steps:
- uses: actions/setup-python@v5
with:
python-version: ${{ matrix.python-version }}Include/Exclude
strategy:
fail-fast: false
matrix:
os: [ubuntu-latest, windows-latest]
node: [18, 20]
include:
- os: ubuntu-latest
node: 20
experimental: true
- os: macos-latest
node: 20
exclude:
- os: windows-latest
node: 18Dynamic matrix
jobs:
generate-matrix:
runs-on: ubuntu-latest
outputs:
matrix: ${{ steps.set-matrix.outputs.matrix }}
steps:
- uses: actions/checkout@v4
- id: set-matrix
run: |
MATRIX=$(cat .github/test-matrix.json)
echo "matrix=$MATRIX" >> $GITHUB_OUTPUT
test:
needs: generate-matrix
strategy:
matrix: ${{ fromJson(needs.generate-matrix.outputs.matrix) }}
runs-on: ubuntu-latest
steps:
- run: echo "Testing ${{ matrix.config }}"---
OIDC Integration
AWS
permissions:
id-token: write
contents: read
steps:
- uses: aws-actions/configure-aws-credentials@v4
with:
role-to-assume: arn:aws:iam::123456789012:role/GitHubActionsRole
aws-region: us-east-1
role-session-name: GitHubActions
- uses: aws-actions/amazon-ecr-login@v2
- name: Deploy to ECS
run: |
aws ecs update-service --cluster my-cluster --service my-service --force-new-deploymentGCP Workload Identity Federation
permissions:
id-token: write
contents: read
steps:
- uses: google-github-actions/auth@v2
with:
workload_identity_provider: ${{ secrets.WIF_PROVIDER }}
service_account: ${{ secrets.WIF_SERVICE_ACCOUNT }}
- uses: google-github-actions/setup-gcloud@v2
- name: Deploy to Cloud Run
run: |
gcloud run deploy my-service \
--image us-central1-docker.pkg.dev/project/repo/image:${{ github.sha }} \
--region us-central1---
Reusable Workflows
Define reusable workflow
# .github/workflows/deploy.yml
name: Reusable Deploy
on:
workflow_call:
inputs:
environment:
required: true
type: string
image-tag:
required: true
type: string
secrets:
deploy-token:
required: true
outputs:
deployment-url:
value: ${{ jobs.deploy.outputs.url }}
jobs:
deploy:
runs-on: ubuntu-latest
outputs:
url: ${{ steps.deploy.outputs.url }}
steps:
- name: Deploy
id: deploy
run: |
echo "Deploying ${{ inputs.image-tag }} to ${{ inputs.environment }}"
echo "url=https://${{ inputs.environment }}.example.com" >> $GITHUB_OUTPUTCall reusable workflow
jobs:
deploy-staging:
uses: ./.github/workflows/deploy.yml
with:
environment: staging
image-tag: ${{ github.sha }}
secrets:
deploy-token: ${{ secrets.STAGING_TOKEN }}
deploy-prod:
needs: deploy-staging
uses: ./.github/workflows/deploy.yml
with:
environment: production
image-tag: ${{ github.sha }}
secrets:
deploy-token: ${{ secrets.PROD_TOKEN }}---
Composite Actions
Create composite action
# .github/actions/setup-project/action.yml
name: 'Setup Project'
description: 'Setup Python, Poetry, and dependencies'
inputs:
python-version:
description: 'Python version'
required: false
default: '3.11'
outputs:
cache-hit:
description: 'Whether cache was hit'
value: ${{ steps.cache.outputs.cache-hit }}
runs:
using: 'composite'
steps:
- uses: actions/setup-python@v5
with:
python-version: ${{ inputs.python-version }}
- uses: actions/cache@v4
id: cache
with:
path: .venv
key: venv-${{ runner.os }}-${{ inputs.python-version }}-${{ hashFiles('poetry.lock') }}
- uses: snok/install-poetry@v1
with:
virtualenvs-in-project: true
- run: poetry install
if: steps.cache.outputs.cache-hit != 'true'
shell: bashUse composite action
steps:
- uses: actions/checkout@v4
- uses: ./.github/actions/setup-project
with:
python-version: '3.11'---
Security Best Practices
Pin actions to SHA
# GOOD: Pinned to commit SHA
- uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
# ACCEPTABLE: Major version
- uses: actions/checkout@v4
# AVOID: Mutable tags
- uses: actions/checkout@mainMinimal permissions
permissions:
contents: read
pull-requests: write
id-token: write # Only for OIDCSecret scanning
- uses: trufflesecurity/trufflehog@main
with:
path: ./
base: main
head: HEAD---
Common Patterns
Path-based conditional execution
- uses: dorny/paths-filter@v3
id: changes
with:
filters: |
backend:
- 'backend/**'
frontend:
- 'frontend/**'
- name: Run backend tests
if: steps.changes.outputs.backend == 'true'
run: npm test --prefix backendEnvironment-specific deployments
jobs:
deploy:
runs-on: ubuntu-latest
environment:
name: production
url: https://example.com
steps:
- run: echo "Deploying..."
env:
API_KEY: ${{ secrets.PROD_API_KEY }}Artifact management
# Upload
- uses: actions/upload-artifact@v4
with:
name: dist
path: dist/
retention-days: 7
# Download
- uses: actions/download-artifact@v4
with:
name: dist
path: dist/Continue on error
- name: Risky step
run: ./might-fail.sh
continue-on-error: true
- name: Always run cleanup
if: always()
run: ./cleanup.sh---
Expression Reference
Conditionals
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
if: contains(github.event.head_commit.message, '[skip ci]') == false
if: success() && github.event.pull_request.draft == false
if: failure() || cancelled()
if: always()
if: startsWith(github.ref, 'refs/tags/v')
if: contains(github.event.pull_request.labels.*.name, 'deploy')Built-in functions
# JSON
${{ fromJson(needs.job.outputs.matrix) }}
${{ toJson(github.event) }}
# Hash files
${{ hashFiles('**/package-lock.json') }}
# Format strings
${{ format('image-{0}:{1}', matrix.variant, github.sha) }}Context values
${{ github.repository }} # owner/repo
${{ github.repository_owner }} # owner
${{ github.sha }} # commit SHA
${{ github.ref }} # refs/heads/main
${{ github.ref_name }} # main
${{ github.actor }} # user who triggered
${{ github.event_name }} # push, pull_request, etc
${{ github.run_id }} # unique run IDOutputs and secrets
${{ needs.build.outputs.version }}
${{ steps.meta.outputs.tags }}
${{ env.NODE_ENV }}
${{ secrets.AWS_ACCESS_KEY_ID }}
${{ vars.API_URL }}---
Production Templates
Full CI/CD Pipeline
name: CI/CD Pipeline
on:
push:
branches: [main, develop]
pull_request:
branches: [main]
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
permissions:
contents: read
pull-requests: write
id-token: write
jobs:
test:
runs-on: ubuntu-latest
timeout-minutes: 10
strategy:
matrix:
node-version: [18, 20]
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: ${{ matrix.node-version }}
cache: 'npm'
- run: npm ci
- run: npm run lint
- run: npm run test:ci
build:
needs: test
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: docker/setup-buildx-action@v3
- uses: docker/build-push-action@v5
with:
context: .
push: false
cache-from: type=gha
cache-to: type=gha,mode=max
deploy:
needs: build
if: github.ref == 'refs/heads/main'
runs-on: ubuntu-latest
environment: production
steps:
- uses: actions/checkout@v4
- run: echo "Deploy to production"Python Monorepo
name: Python Monorepo CI
on:
push:
branches: [main]
pull_request:
jobs:
changes:
runs-on: ubuntu-latest
outputs:
packages: ${{ steps.filter.outputs.changes }}
steps:
- uses: actions/checkout@v4
- uses: dorny/paths-filter@v3
id: filter
with:
filters: |
api:
- 'packages/api/**'
worker:
- 'packages/worker/**'
test:
needs: changes
if: ${{ needs.changes.outputs.packages != '[]' }}
runs-on: ubuntu-latest
strategy:
matrix:
package: ${{ fromJson(needs.changes.outputs.packages) }}
defaults:
run:
working-directory: packages/${{ matrix.package }}
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: '3.11'
- uses: snok/install-poetry@v1
- run: poetry install
- run: poetry run pytest---
Advanced Patterns
Ephemeral PR Environments
Create isolated environments per pull request with automatic cleanup.
Compute Environment Identifier
# .github/actions/compute-identifier/action.yml
name: 'Compute Identifier'
description: 'Compute namespaced environment identifier'
outputs:
identifier:
description: 'Environment identifier'
value: ${{ steps.compute.outputs.identifier }}
is-ephemeral:
description: 'Whether this is an ephemeral environment'
value: ${{ steps.compute.outputs.is-ephemeral }}
runs:
using: 'composite'
steps:
- id: compute
shell: bash
run: |
if [ "${{ github.event_name }}" = "pull_request" ]; then
echo "identifier=pr${{ github.event.number }}" >> $GITHUB_OUTPUT
echo "is-ephemeral=true" >> $GITHUB_OUTPUT
elif [ "${{ github.ref_name }}" = "main" ]; then
echo "identifier=main" >> $GITHUB_OUTPUT
echo "is-ephemeral=false" >> $GITHUB_OUTPUT
else
echo "identifier=${{ github.ref_name }}" >> $GITHUB_OUTPUT
echo "is-ephemeral=true" >> $GITHUB_OUTPUT
fiUse identifier in workflows
jobs:
setup:
runs-on: ubuntu-latest
outputs:
identifier: ${{ steps.id.outputs.identifier }}
is-ephemeral: ${{ steps.id.outputs.is-ephemeral }}
steps:
- uses: actions/checkout@v4
- uses: ./.github/actions/compute-identifier
id: id
deploy:
needs: setup
runs-on: ubuntu-latest
env:
RESOURCE_ID: ${{ needs.setup.outputs.identifier }}
steps:
- run: |
echo "Deploying to environment: $RESOURCE_ID"
# Resources named: myapp-$RESOURCE_ID-*PR Cleanup on Close
Automatically destroy ephemeral resources when PR is closed/merged.
name: Cleanup PR Environment
on:
pull_request:
types: [closed]
jobs:
cleanup:
runs-on: ubuntu-latest
env:
RESOURCE_ID: pr${{ github.event.number }}
steps:
- uses: actions/checkout@v4
- name: Configure AWS credentials
uses: aws-actions/configure-aws-credentials@v4
with:
role-to-assume: ${{ secrets.AWS_ROLE_ARN_CLEANUP }}
aws-region: us-east-1
# Destroy in reverse order: app → schemas → foundation
- name: Destroy application stack
run: |
aws cloudformation delete-stack --stack-name "app-${RESOURCE_ID}" || true
aws cloudformation wait stack-delete-complete --stack-name "app-${RESOURCE_ID}" || true
- name: Destroy foundation stack
run: |
aws cloudformation delete-stack --stack-name "foundation-${RESOURCE_ID}" || true
aws cloudformation wait stack-delete-complete --stack-name "foundation-${RESOURCE_ID}" || true
- name: Clean up SSM parameters
run: |
PARAMS=$(aws ssm get-parameters-by-path --path "/myapp/${RESOURCE_ID}" --recursive --query 'Parameters[].Name' --output text)
if [ -n "$PARAMS" ]; then
aws ssm delete-parameters --names $PARAMS || true
fiRelease Please Integration
Automate semantic versioning based on conventional commits.
name: Release Please
on:
push:
branches: [main]
permissions:
contents: write
pull-requests: write
jobs:
release:
runs-on: ubuntu-latest
outputs:
release_created: ${{ steps.release.outputs.release_created }}
tag_name: ${{ steps.release.outputs.tag_name }}
version: ${{ steps.release.outputs.version }}
steps:
- uses: googleapis/release-please-action@v4
id: release
with:
release-type: node # or: python, simple, etc.
# Deploy only when release is created
deploy:
needs: release
if: ${{ needs.release.outputs.release_created }}
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: |
echo "Deploying version ${{ needs.release.outputs.version }}"Monorepo with manifest
# release-please-config.json
{
"packages": {
"packages/api": {
"release-type": "python"
},
"packages/web": {
"release-type": "node"
}
},
"linked-versions": true
}Testing Patterns
Python with pytest and coverage
name: Python Tests
on: [push, pull_request]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: '3.11'
- name: Install dependencies
run: |
pip install poetry
poetry install
- name: Run tests with coverage
run: |
poetry run pytest -q --cov=src --cov-report=xml --cov-report=term
- name: Upload coverage
uses: codecov/codecov-action@v4
with:
files: ./coverage.xml
fail_ci_if_error: falseJava/Kotlin with Gradle
name: Gradle Tests
on: [push, pull_request]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-java@v4
with:
distribution: 'temurin'
java-version: '17'
cache: 'gradle'
- name: Run tests
run: ./gradlew test check
- name: Publish test report
uses: mikepenz/action-junit-report@v4
if: always()
with:
report_paths: '**/build/test-results/test/TEST-*.xml'TypeScript with Jest
name: TypeScript Tests
on: [push, pull_request]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: '20'
cache: 'npm'
- run: npm ci
- run: npx tsc --noEmit # Type check
- run: npm test -- --coverage
- uses: codecov/codecov-action@v4
with:
files: ./coverage/coverage-final.jsonDeployment Status Checks
Wait for infrastructure to be ready before deploying.
- name: Wait for stack ready
run: |
STACK_NAME="foundation-${RESOURCE_ID}"
MAX_ATTEMPTS=30
ATTEMPT=0
while [ $ATTEMPT -lt $MAX_ATTEMPTS ]; do
STATUS=$(aws cloudformation describe-stacks \
--stack-name "$STACK_NAME" \
--query 'Stacks[0].StackStatus' \
--output text 2>/dev/null || echo "NOT_FOUND")
case "$STATUS" in
CREATE_COMPLETE|UPDATE_COMPLETE)
echo "Stack ready: $STATUS"
exit 0
;;
*_IN_PROGRESS)
echo "Waiting... Status: $STATUS"
sleep 30
;;
*_FAILED|ROLLBACK_*)
echo "Stack failed: $STATUS"
exit 1
;;
NOT_FOUND)
echo "Stack not found, waiting..."
sleep 30
;;
esac
ATTEMPT=$((ATTEMPT + 1))
done
echo "Timeout waiting for stack"
exit 1Separate Build vs Deploy Roles
Use fine-grained OIDC roles for different workflow phases.
jobs:
build:
runs-on: ubuntu-latest
permissions:
id-token: write
contents: read
steps:
- uses: aws-actions/configure-aws-credentials@v4
with:
# Build role: ECR push + SSM write only
role-to-assume: ${{ secrets.AWS_ROLE_ARN_BUILD }}
aws-region: us-east-1
- name: Build and push image
run: |
docker build -t $ECR_REPO:$VERSION .
docker push $ECR_REPO:$VERSION
aws ssm put-parameter --name "/app/version" --value "$VERSION" --overwrite
deploy:
needs: build
runs-on: ubuntu-latest
permissions:
id-token: write
contents: read
steps:
- uses: aws-actions/configure-aws-credentials@v4
with:
# Deploy role: CloudFormation + broader permissions
role-to-assume: ${{ secrets.AWS_ROLE_ARN_DEPLOY }}
aws-region: us-east-1
- name: Deploy stack
run: npx cdk deploy --require-approval neverOrdered Multi-Stack Deployment
Deploy stacks in dependency order with wait checks.
name: Deploy All Stacks
on:
workflow_dispatch:
push:
branches: [main]
jobs:
foundation:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: npx cdk deploy FoundationStack --require-approval never
schemas:
needs: foundation
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: npx cdk deploy SchemasStack --require-approval never
application:
needs: schemas
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: npx cdk deploy ApplicationStack --require-approval never---
Workflow Summaries
Use $GITHUB_STEP_SUMMARY for rich markdown summaries in workflow runs.
Basic Summary
- name: Generate summary
run: |
echo "## Build Results" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "| Metric | Value |" >> $GITHUB_STEP_SUMMARY
echo "|--------|-------|" >> $GITHUB_STEP_SUMMARY
echo "| Tests | 42 passed |" >> $GITHUB_STEP_SUMMARY
echo "| Coverage | 85% |" >> $GITHUB_STEP_SUMMARY
echo "| Build time | 2m 30s |" >> $GITHUB_STEP_SUMMARYDeployment Summary with Links
- name: Deployment summary
run: |
cat >> $GITHUB_STEP_SUMMARY << 'EOF'
## Deployment Complete
| Environment | URL | Status |
|------------|-----|--------|
| API | https://api-${{ env.ENV_ID }}.example.com | ✅ |
| Web | https://web-${{ env.ENV_ID }}.example.com | ✅ |
### Resources Created
- Database: `myapp-${{ env.ENV_ID }}-db`
- Cache: `myapp-${{ env.ENV_ID }}-redis`
> **Note**: PR environments auto-cleanup when PR is closed
EOFTest Results Summary
- name: Test results summary
if: always()
run: |
echo "## Test Results" >> $GITHUB_STEP_SUMMARY
if [ -f test-results.json ]; then
PASSED=$(jq '.passed' test-results.json)
FAILED=$(jq '.failed' test-results.json)
echo "" >> $GITHUB_STEP_SUMMARY
if [ "$FAILED" -eq 0 ]; then
echo "✅ All $PASSED tests passed!" >> $GITHUB_STEP_SUMMARY
else
echo "❌ $FAILED tests failed out of $((PASSED + FAILED))" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "### Failed Tests" >> $GITHUB_STEP_SUMMARY
jq -r '.failures[] | "- \(.name): \(.message)"' test-results.json >> $GITHUB_STEP_SUMMARY
fi
fi---
Container Security
Multi-Stage Builds
Minimize attack surface with multi-stage Docker builds.
# Build stage
FROM python:3.11-slim AS builder
WORKDIR /app
COPY pyproject.toml poetry.lock ./
RUN pip install poetry && poetry export -f requirements.txt -o requirements.txt
COPY . .
RUN pip install --no-cache-dir -r requirements.txt
# Runtime stage - minimal image
FROM python:3.11-slim AS runtime
WORKDIR /app
# Non-root user
RUN addgroup --system app && adduser --system --ingroup app app
USER app
# Copy only what's needed
COPY --from=builder /app/dist ./dist
COPY --from=builder /usr/local/lib/python3.11/site-packages /usr/local/lib/python3.11/site-packages
ENTRYPOINT ["python", "-m", "app"]Container Scanning with Trivy
name: Container Security
on:
push:
paths:
- 'Dockerfile'
- '.github/workflows/container-security.yml'
pull_request:
jobs:
scan:
runs-on: ubuntu-latest
permissions:
security-events: write
steps:
- uses: actions/checkout@v4
- name: Build image
run: docker build -t myapp:${{ github.sha }} .
- name: Run Trivy vulnerability scan
uses: aquasecurity/trivy-action@master
with:
image-ref: myapp:${{ github.sha }}
format: 'sarif'
output: 'trivy-results.sarif'
severity: 'CRITICAL,HIGH'
- name: Upload scan results
uses: github/codeql-action/upload-sarif@v3
with:
sarif_file: 'trivy-results.sarif'Image Tagging Strategy
- name: Docker metadata
id: meta
uses: docker/metadata-action@v5
with:
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
tags: |
# Branch name
type=ref,event=branch
# PR number
type=ref,event=pr
# Semver tags
type=semver,pattern={{version}}
type=semver,pattern={{major}}.{{minor}}
# SHA for traceability
type=sha,prefix=
- name: Build and push
uses: docker/build-push-action@v5
with:
context: .
push: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}---
Security Scanning
CodeQL Analysis
name: CodeQL
on:
push:
branches: [main]
pull_request:
branches: [main]
schedule:
- cron: '0 6 * * 1' # Weekly Monday 6 AM
jobs:
analyze:
runs-on: ubuntu-latest
permissions:
security-events: write
contents: read
actions: read
strategy:
matrix:
language: [python, javascript]
steps:
- uses: actions/checkout@v4
- name: Initialize CodeQL
uses: github/codeql-action/init@v3
with:
languages: ${{ matrix.language }}
queries: +security-extended
- name: Autobuild
uses: github/codeql-action/autobuild@v3
- name: Perform analysis
uses: github/codeql-action/analyze@v3
with:
category: "/language:${{ matrix.language }}"Dependabot Configuration
# .github/dependabot.yml
version: 2
registries:
npm-registry:
type: npm-registry
url: https://npm.pkg.github.com
token: ${{ secrets.GITHUB_TOKEN }}
updates:
# Python dependencies
- package-ecosystem: "pip"
directory: "/"
schedule:
interval: "weekly"
day: "monday"
groups:
dev-dependencies:
patterns:
- "pytest*"
- "ruff"
- "mypy"
ignore:
- dependency-name: "boto3"
update-types: ["version-update:semver-patch"]
# GitHub Actions
- package-ecosystem: "github-actions"
directory: "/"
schedule:
interval: "weekly"
groups:
actions:
patterns:
- "*"
# Docker base images
- package-ecosystem: "docker"
directory: "/"
schedule:
interval: "weekly"Infrastructure-as-Code Scanning
Scan CDK, Pulumi, Terraform, and CloudFormation for misconfigurations.
name: IaC Security
on:
push:
paths:
- 'infra/**'
- 'cdk/**'
- '*.tf'
pull_request:
jobs:
checkov:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Run Checkov
uses: bridgecrewio/checkov-action@master
with:
directory: infra/
framework: cloudformation,terraform
soft_fail: false
output_format: sarif
output_file_path: checkov-results.sarif
- name: Upload results
uses: github/codeql-action/upload-sarif@v3
if: always()
with:
sarif_file: checkov-results.sarif
tfsec:
runs-on: ubuntu-latest
if: hashFiles('**/*.tf') != ''
steps:
- uses: actions/checkout@v4
- name: Run tfsec
uses: aquasecurity/tfsec-action@v1.0.0
with:
soft_fail: trueBranch Protection Status
Update commit status for branch protection integration.
- name: Set commit status
uses: actions/github-script@v7
with:
script: |
await github.rest.repos.createCommitStatus({
owner: context.repo.owner,
repo: context.repo.repo,
sha: context.sha,
state: 'success',
context: 'security/scan',
description: 'All security checks passed',
target_url: `${process.env.GITHUB_SERVER_URL}/${context.repo.owner}/${context.repo.repo}/actions/runs/${context.runId}`
});---
Troubleshooting Guide
Flaky Test Handling
- name: Run tests with retry
uses: nick-fields/retry@v3
with:
timeout_minutes: 10
max_attempts: 3
retry_on: error
command: npm test
- name: Run tests (alternative)
run: |
for i in 1 2 3; do
npm test && break
echo "Attempt $i failed, retrying..."
sleep 5
doneDebug Mode
# Enable step debug logging
env:
ACTIONS_STEP_DEBUG: true
ACTIONS_RUNNER_DEBUG: true
# Conditional debug output
- name: Debug information
if: runner.debug == '1'
run: |
echo "Event: ${{ github.event_name }}"
echo "Ref: ${{ github.ref }}"
echo "SHA: ${{ github.sha }}"
env | sortSSH Debug Access
- name: Setup tmate session
uses: mxschmitt/action-tmate@v3
if: failure()
timeout-minutes: 15
with:
limit-access-to-actor: trueCommon Issues and Solutions
| Issue | Cause | Solution |
|---|---|---|
Permission denied | Missing OIDC permissions | Add id-token: write to permissions |
Resource not found | Wrong region/account | Verify AWS_REGION and role ARN |
Rate limit exceeded | Too many API calls | Add delays, use caching |
Disk space full | Large artifacts/caches | Clean workspace, prune Docker |
Timeout exceeded | Long-running step | Increase timeout-minutes |
Cache miss | Key mismatch | Check hashFiles() paths |
Disk Space Cleanup
- name: Free disk space
run: |
sudo rm -rf /usr/share/dotnet
sudo rm -rf /opt/ghc
sudo rm -rf /usr/local/share/boost
docker system prune -af
df -hSlow Build Optimization
# Use parallel jobs
jobs:
lint:
runs-on: ubuntu-latest
steps:
- run: npm run lint
test:
runs-on: ubuntu-latest
steps:
- run: npm test
build:
runs-on: ubuntu-latest
steps:
- run: npm run build
# All run in parallel, total time = max(lint, test, build)---
Performance Reference
GitHub Actions Limits
| Resource | Limit | Notes |
|---|---|---|
| Workflow run time | 6 hours | Per workflow |
| Job execution time | 6 hours | Per job |
| API requests | 1,000/hour | Per repository |
| Concurrent jobs | 20 (free) / 500 (enterprise) | Per account |
| Matrix jobs | 256 | Per workflow |
| Artifact storage | 500 MB (free) / 2 GB (pro) | Per artifact |
| Artifact retention | 90 days (default) | Configurable 1-400 days |
| Cache size | 10 GB | Per repository |
| Log retention | 400 days (public) / 90 days (private) |
Runner Specifications
| Runner | vCPU | RAM | Storage | Cost |
|---|---|---|---|---|
| ubuntu-latest | 4 | 16 GB | 14 GB SSD | Free tier available |
| ubuntu-24.04 | 4 | 16 GB | 14 GB SSD | Free tier available |
| windows-latest | 4 | 16 GB | 14 GB SSD | 2x Linux minutes |
| macos-latest | 3 | 14 GB | 14 GB SSD | 10x Linux minutes |
| macos-14 (M1) | 3 | 7 GB | 14 GB SSD | 10x Linux minutes |
Optimization Tips
| Technique | Time Saved | Implementation |
|---|---|---|
| Dependency caching | 30-70% | actions/cache with hashFiles |
| Parallel jobs | 40-60% | Split independent work |
| Docker layer caching | 50-80% | cache-from: type=gha |
| Shallow clone | 10-30% | fetch-depth: 1 |
| Skip unnecessary runs | 100% | paths-ignore, conditional |
| Self-hosted runners | Variable | For specialized hardware |
Package Manager Detection
Dynamically detect and use the correct package manager.
- name: Detect package manager
id: detect-pm
run: |
if [ -f "pnpm-lock.yaml" ]; then
echo "manager=pnpm" >> $GITHUB_OUTPUT
echo "command=pnpm install --frozen-lockfile" >> $GITHUB_OUTPUT
elif [ -f "yarn.lock" ]; then
echo "manager=yarn" >> $GITHUB_OUTPUT
echo "command=yarn install --frozen-lockfile" >> $GITHUB_OUTPUT
else
echo "manager=npm" >> $GITHUB_OUTPUT
echo "command=npm ci" >> $GITHUB_OUTPUT
fi
- uses: actions/setup-node@v4
with:
node-version: '20'
cache: ${{ steps.detect-pm.outputs.manager }}
- run: ${{ steps.detect-pm.outputs.command }}---
Workflow Checklist
Before deploying a new workflow, verify:
- [ ] Uses concurrency control to prevent parallel runs
- [ ] Has explicit permissions (not default)
- [ ] Actions pinned to SHA or major version
- [ ] Secrets accessed via ${{ secrets.* }}
- [ ] Environment variables use ${{ env.* }} or ${{ vars.* }}
- [ ] Cleanup jobs tolerate failures (|| true)
- [ ] Tests run before deployment
- [ ] PR environments auto-cleanup on close
- [ ] Build and deploy use separate OIDC roles
- [ ] Stacks deploy in dependency order#!/bin/bash
# batch-search.sh - Search GitHub with >1000 results using date partitioning
#
# GitHub Search API limits results to 1000 per query. This script works around
# that limitation by splitting searches into date ranges and combining results.
#
# Usage: ./batch-search.sh <query> <start-date> <end-date> [partition-days] [type]
# query - Search query (without date qualifiers)
# start-date - Start date (YYYY-MM-DD)
# end-date - End date (YYYY-MM-DD)
# partition-days - Optional: days per partition (default: 30)
# type - Optional: 'repos' or 'code' (default: repos)
#
# Examples:
# ./batch-search.sh "language:python topic:cli" 2024-01-01 2024-12-31
# ./batch-search.sh "language:go stars:>100" 2023-01-01 2024-12-31 60
# ./batch-search.sh "filename:SKILL.md" 2024-01-01 2024-06-30 30 code
#
# Output: JSON array of all unique results
#
# Exit codes:
# 0 - Success
# 1 - Error
set -euo pipefail
# Dependency check
for cmd in gh jq date; do
command -v "$cmd" &>/dev/null || { echo "Error: $cmd required but not found" >&2; exit 1; }
done
# Verify gh authentication
gh auth status &>/dev/null || { echo "Error: gh not authenticated. Run: gh auth login" >&2; exit 1; }
# Arguments
QUERY=${1:?Usage: $0 <query> <start-date> <end-date> [partition-days] [type]}
START_DATE=${2:?Usage: $0 <query> <start-date> <end-date> [partition-days] [type]}
END_DATE=${3:?Usage: $0 <query> <start-date> <end-date> [partition-days] [type]}
PARTITION_DAYS=${4:-30}
SEARCH_TYPE=${5:-repos}
# Validate search type
case "$SEARCH_TYPE" in
repos|code) ;;
*) echo "Error: type must be 'repos' or 'code'" >&2; exit 1 ;;
esac
# Validate dates
validate_date() {
if ! date -d "$1" &>/dev/null; then
echo "Error: Invalid date format: $1 (use YYYY-MM-DD)" >&2
exit 1
fi
}
validate_date "$START_DATE"
validate_date "$END_DATE"
# Convert dates to seconds for comparison
start_seconds=$(date -d "$START_DATE" +%s)
end_seconds=$(date -d "$END_DATE" +%s)
if [ "$start_seconds" -ge "$end_seconds" ]; then
echo "Error: start-date must be before end-date" >&2
exit 1
fi
# Calculate partition size in seconds
partition_seconds=$((PARTITION_DAYS * 86400))
# Temp file for collecting results
RESULTS_FILE=$(mktemp)
trap 'rm -f "$RESULTS_FILE"' EXIT
# Rate limit delay based on search type
if [ "$SEARCH_TYPE" = "code" ]; then
DELAY=6 # Code search: 10/minute
else
DELAY=2 # Repo search: 30/minute
fi
# Function to search a date range
search_range() {
local range_start=$1
local range_end=$2
local range_start_fmt=$(date -d "@$range_start" +%Y-%m-%d)
local range_end_fmt=$(date -d "@$range_end" +%Y-%m-%d)
local date_qualifier
if [ "$SEARCH_TYPE" = "repos" ]; then
date_qualifier="created:${range_start_fmt}..${range_end_fmt}"
else
# Code search uses pushed date
date_qualifier="pushed:${range_start_fmt}..${range_end_fmt}"
fi
local full_query="$QUERY $date_qualifier"
echo " Searching: $range_start_fmt to $range_end_fmt" >&2
local result
if [ "$SEARCH_TYPE" = "repos" ]; then
result=$(gh search repos "$full_query" --limit 1000 --json fullName,url,createdAt 2>/dev/null) || result="[]"
else
result=$(gh search code "$full_query" --limit 100 --json repository,path 2>/dev/null) || result="[]"
fi
local count=$(echo "$result" | jq 'length')
echo " Found: $count results" >&2
# Warn if hitting limit
if [ "$count" -ge 1000 ] && [ "$SEARCH_TYPE" = "repos" ]; then
echo " Warning: Hit 1000 result limit. Consider smaller partitions." >&2
elif [ "$count" -ge 100 ] && [ "$SEARCH_TYPE" = "code" ]; then
echo " Warning: Hit 100 result limit for code search." >&2
fi
echo "$result"
}
# Main execution
main() {
echo "Batch search starting..." >&2
echo "Query: $QUERY" >&2
echo "Range: $START_DATE to $END_DATE" >&2
echo "Partition: ${PARTITION_DAYS} days" >&2
echo "Type: $SEARCH_TYPE" >&2
echo "" >&2
# Initialize results array
echo "[]" > "$RESULTS_FILE"
# Iterate through date partitions
current=$start_seconds
partition_count=0
while [ "$current" -lt "$end_seconds" ]; do
partition_count=$((partition_count + 1))
# Calculate partition end
partition_end=$((current + partition_seconds))
if [ "$partition_end" -gt "$end_seconds" ]; then
partition_end=$end_seconds
fi
# Search this partition
partition_results=$(search_range "$current" "$partition_end")
# Merge results
jq -s 'add' "$RESULTS_FILE" <(echo "$partition_results") > "${RESULTS_FILE}.tmp"
mv "${RESULTS_FILE}.tmp" "$RESULTS_FILE"
# Move to next partition
current=$((partition_end + 86400)) # +1 day to avoid overlap
# Rate limit delay (skip after last partition)
if [ "$current" -lt "$end_seconds" ]; then
sleep "$DELAY"
fi
done
echo "" >&2
echo "Searched $partition_count partitions" >&2
# Deduplicate and output
if [ "$SEARCH_TYPE" = "repos" ]; then
# Dedupe by fullName for repos
jq 'unique_by(.fullName) | sort_by(.fullName)' "$RESULTS_FILE"
final_count=$(jq 'unique_by(.fullName) | length' "$RESULTS_FILE")
else
# Dedupe by repo+path for code
jq 'unique_by(.repository.fullName + .path) | sort_by(.repository.fullName)' "$RESULTS_FILE"
final_count=$(jq 'unique_by(.repository.fullName + .path) | length' "$RESULTS_FILE")
fi
echo "" >&2
echo "Total unique results: $final_count" >&2
}
main
#!/bin/bash
# find-repos-with-path.sh - Find GitHub repos containing a specific directory/path
#
# Usage: ./find-repos-with-path.sh <path> [owner] [format]
# path - Path to search for (e.g., ".skilz", ".cursor", ".codex")
# owner - Optional: owner/org to scope search
# format - Optional: output format (names|urls|json) - default: names
#
# Examples:
# ./find-repos-with-path.sh .skilz
# ./find-repos-with-path.sh .cursor myorg
# ./find-repos-with-path.sh .codex myorg urls
# ./find-repos-with-path.sh src/components "" json
#
# Exit codes:
# 0 - Success
# 1 - Error or no results
set -euo pipefail
# Dependency check
for cmd in gh jq; do
command -v "$cmd" &>/dev/null || { echo "Error: $cmd required but not found" >&2; exit 1; }
done
# Verify gh authentication
gh auth status &>/dev/null || { echo "Error: gh not authenticated. Run: gh auth login" >&2; exit 1; }
# Arguments
PATH_QUERY=${1:?Usage: $0 <path> [owner] [format]}
OWNER=${2:-}
FORMAT=${3:-names}
# Validate format
case "$FORMAT" in
names|urls|json) ;;
*) echo "Error: format must be 'names', 'urls', or 'json'" >&2; exit 1 ;;
esac
# Build search command
SEARCH_ARGS=("search" "code" "path:$PATH_QUERY")
if [ -n "$OWNER" ]; then
SEARCH_ARGS+=("--owner" "$OWNER")
fi
SEARCH_ARGS+=("--json" "repository" "--limit" "100")
# Function to deduplicate and format results
format_results() {
case "$FORMAT" in
names)
jq -r '.[].repository.fullName' | sort -u
;;
urls)
jq -r '.[].repository.url' | sort -u
;;
json)
jq '[.[].repository | {name: .fullName, url: .url}] | unique_by(.name)'
;;
esac
}
# Check rate limit before starting
check_rate_limit() {
local remaining
remaining=$(gh api rate_limit --jq '.resources.code_search.remaining' 2>/dev/null || echo "100")
if [ "$remaining" -lt 5 ]; then
echo "Warning: Code search rate limit low ($remaining remaining)" >&2
local reset
reset=$(gh api rate_limit --jq '.resources.code_search.reset')
local wait_time=$((reset - $(date +%s)))
if [ $wait_time -gt 0 ] && [ $wait_time -lt 120 ]; then
echo "Waiting ${wait_time}s for rate limit reset..." >&2
sleep "$wait_time"
fi
fi
}
# Main execution
main() {
check_rate_limit
echo "Searching for repos with path: $PATH_QUERY" >&2
[ -n "$OWNER" ] && echo "Scoped to owner: $OWNER" >&2
if ! results=$(gh "${SEARCH_ARGS[@]}" 2>&1); then
echo "Error: Search failed - $results" >&2
exit 1
fi
# Check for empty results
count=$(echo "$results" | jq 'length')
if [ "$count" -eq 0 ]; then
echo "No repositories found with path: $PATH_QUERY" >&2
exit 0
fi
echo "Found $count results (deduplicating...)" >&2
# Format and output
echo "$results" | format_results
# Show unique count
unique_count=$(echo "$results" | jq '[.[].repository.fullName] | unique | length')
echo "Total unique repos: $unique_count" >&2
}
main
#!/bin/bash
# wait-for-run.sh - Wait for a GitHub Actions workflow run to complete
#
# Usage: ./wait-for-run.sh <run-id> [timeout] [interval]
# run-id - The workflow run ID to watch
# timeout - Optional: timeout in seconds (default: 3600 = 1 hour)
# interval - Optional: polling interval in seconds (default: 30)
#
# Exit codes:
# 0 - Run completed successfully
# 1 - Run failed or error occurred
# 2 - Run was cancelled
# 3 - Timeout reached
#
# Examples:
# ./wait-for-run.sh 12345678
# ./wait-for-run.sh 12345678 1800 # 30 minute timeout
# ./wait-for-run.sh 12345678 3600 10 # 1 hour timeout, check every 10s
set -euo pipefail
# Dependency check
for cmd in gh jq; do
command -v "$cmd" &>/dev/null || { echo "Error: $cmd required but not found" >&2; exit 1; }
done
# Verify gh authentication
gh auth status &>/dev/null || { echo "Error: gh not authenticated. Run: gh auth login" >&2; exit 1; }
# Arguments
RUN_ID=${1:?Usage: $0 <run-id> [timeout] [interval]}
TIMEOUT=${2:-3600}
INTERVAL=${3:-30}
# Validate run ID is numeric
if ! [[ "$RUN_ID" =~ ^[0-9]+$ ]]; then
echo "Error: run-id must be numeric" >&2
exit 1
fi
# Track start time
START_TIME=$(date +%s)
# Function to get run status
get_run_status() {
gh run view "$RUN_ID" --json status,conclusion 2>/dev/null
}
# Function to check if timed out
check_timeout() {
local elapsed=$(($(date +%s) - START_TIME))
if [ "$elapsed" -ge "$TIMEOUT" ]; then
return 0 # Timed out
fi
return 1 # Not timed out
}
# Function to format duration
format_duration() {
local seconds=$1
local minutes=$((seconds / 60))
local remaining_seconds=$((seconds % 60))
if [ "$minutes" -gt 0 ]; then
echo "${minutes}m ${remaining_seconds}s"
else
echo "${remaining_seconds}s"
fi
}
# Main wait loop
main() {
echo "Waiting for run $RUN_ID to complete..."
echo "Timeout: $(format_duration $TIMEOUT), Interval: ${INTERVAL}s"
echo ""
# Verify run exists
if ! status_json=$(get_run_status); then
echo "Error: Could not find run $RUN_ID" >&2
exit 1
fi
while true; do
# Check timeout
if check_timeout; then
local elapsed=$(($(date +%s) - START_TIME))
echo ""
echo "Timeout reached after $(format_duration $elapsed)"
exit 3
fi
# Get current status
if ! status_json=$(get_run_status); then
echo "Error: Failed to get run status" >&2
exit 1
fi
status=$(echo "$status_json" | jq -r '.status')
conclusion=$(echo "$status_json" | jq -r '.conclusion')
elapsed=$(($(date +%s) - START_TIME))
# Print progress
printf "\r[%s] Status: %-12s Elapsed: %s " \
"$(date +%H:%M:%S)" "$status" "$(format_duration $elapsed)"
# Check if completed
if [ "$status" = "completed" ]; then
echo ""
echo ""
echo "Run completed with conclusion: $conclusion"
case "$conclusion" in
success)
echo "✓ Run succeeded"
exit 0
;;
failure)
echo "✗ Run failed"
echo ""
echo "Failed step logs:"
gh run view "$RUN_ID" --log-failed 2>/dev/null | tail -50 || true
exit 1
;;
cancelled)
echo "⊘ Run was cancelled"
exit 2
;;
skipped)
echo "⊘ Run was skipped"
exit 0
;;
*)
echo "? Unknown conclusion: $conclusion"
exit 1
;;
esac
fi
# Wait for next poll
sleep "$INTERVAL"
done
}
# Handle interrupt
trap 'echo ""; echo "Interrupted. Run $RUN_ID may still be in progress."; exit 130' INT TERM
main