Back to skills
SKILL.md
Github Project Management
ASecurityGitHub project management operations using gh CLI and GraphQL. Covers Projects v2 boards, milestones, issue relationships, labels, status reporting, and sprint planning. Used by the project-manager agent to coordinate work.
- 2 stars
- 0 votes
- 0 copies
- 2 views
- Added September 19, 2026
Works with
Security analysis
100/100npx -y skills add dustinkeeton/wafflestack --skill github-project-management --agent claude-codeAre you the author of Github Project Management?
Add the live security badge to your README. It updates with every re-scan.
[](https://www.skillsdirectory.com/skills/dustinkeeton-github-project-management)---
name: github-project-management
description: GitHub project management operations using gh CLI and GraphQL. Covers Projects v2 boards, milestones, issue relationships, labels, status reporting, and sprint planning. Used by the project-manager agent to coordinate work.
user-invocable: false
---
# GitHub Project Management
Operations for managing GitHub Projects v2, milestones, labels, issue relationships, and sprint planning using the `gh` CLI. Projects v2 requires GraphQL via `gh api graphql` since the REST API has limited support.
## Prerequisites
- `gh` CLI authenticated with appropriate scopes (`project`, `repo`)
- Repository owner and name available via `gh repo view --json owner,name`
## Projects v2 Operations
### Discover the project
Find the project ID (required for all subsequent operations):
```bash
# List projects for the repo owner
gh api graphql -f query='
query($owner: String!) {
user(login: $owner) {
projectsV2(first: 20) {
nodes { id number title }
}
}
}
' -f owner="$(gh repo view --json owner -q .owner.login)"
```
For organization-owned repos, replace `user` with `organization`.
### Get project fields
Retrieve custom field IDs (Status, Priority, Sprint, etc.):
```bash
gh api graphql -f query='
query($projectId: ID!) {
node(id: $projectId) {
... on ProjectV2 {
fields(first: 50) {
nodes {
... on ProjectV2Field { id name }
... on ProjectV2SingleSelectField {
id name
options { id name }
}
... on ProjectV2IterationField {
id name
configuration {
iterations { id title startDate duration }
}
}
}
}
}
}
}
' -f projectId="PROJECT_ID"
```
### Add an issue to the project
```bash
gh api graphql -f query='
mutation($projectId: ID!, $contentId: ID!) {
addProjectV2ItemById(input: {projectId: $projectId, contentId: $contentId}) {
item { id }
}
}
' -f projectId="PROJECT_ID" -f contentId="ISSUE_NODE_ID"
```
Get the issue's node ID first:
```bash
gh api graphql -f query='
query($owner: String!, $repo: String!, $number: Int!) {
repository(owner: $owner, name: $repo) {
issue(number: $number) { id }
}
}
' -f owner="OWNER" -f repo="REPO" -F number=42
```
### Update item status (single-select field)
```bash
gh api graphql -f query='
mutation($projectId: ID!, $itemId: ID!, $fieldId: ID!, $optionId: String!) {
updateProjectV2ItemFieldValue(input: {
projectId: $projectId
itemId: $itemId
fieldId: $fieldId
value: {singleSelectOptionId: $optionId}
}) {
projectV2Item { id }
}
}
' -f projectId="PROJECT_ID" -f itemId="ITEM_ID" -f fieldId="STATUS_FIELD_ID" -f optionId="OPTION_ID"
```
### Update item date field (Start / Target)
```bash
gh api graphql -f query='
mutation($projectId: ID!, $itemId: ID!, $fieldId: ID!, $date: Date!) {
updateProjectV2ItemFieldValue(input: {
projectId: $projectId
itemId: $itemId
fieldId: $fieldId
value: {date: $date}
}) {
projectV2Item { id }
}
}
' -f projectId="PROJECT_ID" -f itemId="ITEM_ID" -f fieldId="DATE_FIELD_ID" -f date="2026-04-01"
```
### Batch-update dates using aliased mutations
Update multiple items in a single API call using GraphQL aliases. Up to ~20 operations per call:
```bash
gh api graphql -f query='
mutation {
s1: updateProjectV2ItemFieldValue(input: {
projectId: "PROJECT_ID", itemId: "ITEM_1", fieldId: "START_FIELD_ID",
value: {date: "2026-03-20"}
}) { projectV2Item { id } }
t1: updateProjectV2ItemFieldValue(input: {
projectId: "PROJECT_ID", itemId: "ITEM_1", fieldId: "TARGET_FIELD_ID",
value: {date: "2026-03-25"}
}) { projectV2Item { id } }
s2: updateProjectV2ItemFieldValue(input: {
projectId: "PROJECT_ID", itemId: "ITEM_2", fieldId: "START_FIELD_ID",
value: {date: "2026-03-22"}
}) { projectV2Item { id } }
t2: updateProjectV2ItemFieldValue(input: {
projectId: "PROJECT_ID", itemId: "ITEM_2", fieldId: "TARGET_FIELD_ID",
value: {date: "2026-03-28"}
}) { projectV2Item { id } }
}
'
```
### Query items with date fields
```bash
gh api graphql -f query='
query($projectId: ID!) {
node(id: $projectId) {
... on ProjectV2 {
items(first: 100) {
nodes {
id
content { ... on Issue { number title } }
fieldValues(first: 20) {
nodes {
... on ProjectV2ItemFieldDateValue {
field { ... on ProjectV2Field { name } }
date
}
... on ProjectV2ItemFieldSingleSelectValue {
field { ... on ProjectV2SingleSelectField { name } }
name
}
}
}
}
}
}
}
}
' -f projectId="PROJECT_ID"
```
### Clear a date field
Set the date value to `null` (empty string) to clear it:
```bash
gh api graphql -f query='
mutation($projectId: ID!, $itemId: ID!, $fieldId: ID!) {
clearProjectV2ItemFieldValue(input: {
projectId: $projectId
itemId: $itemId
fieldId: $fieldId
}) {
projectV2Item { id }
}
}
' -f projectId="PROJECT_ID" -f itemId="ITEM_ID" -f fieldId="DATE_FIELD_ID"
```
### Predict dates for items
When assigning predicted Start and Target dates, use these heuristics:
1. **Done items** — use actual `createdAt` as Start and `closedAt` as Target (from `gh issue list --state closed --json number,createdAt,closedAt`)
2. **In Review / In Progress** — Start = when work began (estimate from PR open date or status change), Target = 3-7 days from now depending on size
3. **Todo** — Start = next available slot after current In Progress items finish
4. **Backlog** — schedule within the milestone window:
- **Priority order**: Critical → High → Medium → Low → unset
- **Size → duration**: S = 2-3 days, M = 5 days, L = 10 days, unset = 5 days
- **Dependencies**: items that block others go first
- **Parallel capacity**: assume 2-3 items can be worked in parallel
5. **No milestone** — schedule after all milestoned work
### Update item iteration (sprint)
```bash
gh api graphql -f query='
mutation($projectId: ID!, $itemId: ID!, $fieldId: ID!, $iterationId: String!) {
updateProjectV2ItemFieldValue(input: {
projectId: $projectId
itemId: $itemId
fieldId: $fieldId
value: {iterationId: $iterationId}
}) {
projectV2Item { id }
}
}
' -f projectId="PROJECT_ID" -f itemId="ITEM_ID" -f fieldId="ITERATION_FIELD_ID" -f iterationId="ITERATION_ID"
```
### Query project items with status
```bash
gh api graphql -f query='
query($projectId: ID!) {
node(id: $projectId) {
... on ProjectV2 {
items(first: 100) {
nodes {
id
content {
... on Issue { number title state }
... on PullRequest { number title state }
}
fieldValues(first: 20) {
nodes {
... on ProjectV2ItemFieldSingleSelectValue {
field { ... on ProjectV2SingleSelectField { name } }
name
}
... on ProjectV2ItemFieldIterationValue {
field { ... on ProjectV2IterationField { name } }
title
}
}
}
}
}
}
}
}
' -f projectId="PROJECT_ID"
```
### Remove an item from the project
```bash
gh api graphql -f query='
mutation($projectId: ID!, $itemId: ID!) {
deleteProjectV2Item(input: {projectId: $projectId, itemId: $itemId}) {
deletedItemId
}
}
' -f projectId="PROJECT_ID" -f itemId="ITEM_ID"
```
## Milestones
Milestones use the REST API via `gh api`.
### Create a milestone
```bash
gh api repos/{owner}/{repo}/milestones \
-f title="v0.2.0" \
-f description="Audio transcription and elaboration improvements" \
-f due_on="2026-04-01T00:00:00Z" \
-f state="open"
```
### List milestones
```bash
gh api repos/{owner}/{repo}/milestones --jq '.[] | {number, title, open_issues, closed_issues, due_on, state}'
```
### Update a milestone
```bash
gh api repos/{owner}/{repo}/milestones/{milestone_number} \
-X PATCH \
-f title="v0.2.0 - Revised" \
-f state="open"
```
### Close a milestone
```bash
gh api repos/{owner}/{repo}/milestones/{milestone_number} \
-X PATCH \
-f state="closed"
```
### Assign an issue to a milestone
```bash
gh api repos/{owner}/{repo}/issues/{issue_number} \
-X PATCH \
-F milestone={milestone_number}
```
### Get milestone progress
```bash
gh api repos/{owner}/{repo}/milestones/{milestone_number} \
--jq '{title, open_issues, closed_issues, progress: ((.closed_issues / ((.open_issues + .closed_issues) | if . == 0 then 1 else . end)) * 100 | floor | tostring + "%")}'
```
## Issue Relationships
### Task lists (parent/child)
Create parent-child relationships using task lists in the issue body. Edit an issue body to include tracked tasks:
```bash
gh issue edit {parent_number} --body "$(cat <<'EOF'
## Tasks
- [ ] #{child_1}
- [ ] #{child_2}
- [ ] #{child_3}
EOF
)"
```
GitHub automatically tracks completion of referenced issues in task lists.
### Cross-references (dependencies)
Add dependency notes as comments:
```bash
gh issue comment {issue_number} --body "Blocked by #42. This issue should be worked on after #42 is resolved."
```
### Link related issues
Reference related issues in comments or the issue body:
```bash
gh issue comment {issue_number} --body "Related to #15 and #23."
```
### Query sub-issues of a parent
```bash
gh api graphql -f query='
query($owner: String!, $repo: String!, $number: Int!) {
repository(owner: $owner, name: $repo) {
issue(number: $number) {
trackedIssues(first: 50) {
nodes { number title state }
}
}
}
}
' -f owner="OWNER" -f repo="REPO" -F number=10
```
## Labels
### Create a label
```bash
gh label create "sprint:current" --description "Items in the current sprint" --color "0E8A16"
gh label create "priority: high" --description "High priority item" --color "D93F0B"
gh label create "priority: medium" --description "Medium priority item" --color "FBCA04"
gh label create "priority: low" --description "Low priority item" --color "0075CA"
```
### List labels
```bash
gh label list
```
### Apply labels to an issue
```bash
gh issue edit {issue_number} --add-label "priority: high,sprint:current"
```
### Remove labels from an issue
```bash
gh issue edit {issue_number} --remove-label "sprint:current"
```
### Query issues by label
```bash
gh issue list --label "priority: high" --state open --json number,title,labels
```
### Query issues by multiple labels
```bash
gh issue list --label "priority: high" --label "sprint:current" --state open --json number,title
```
## Status Reporting
### Sprint progress report
Generate a summary of all open issues grouped by status:
```bash
# Get all open issues with labels and milestone
gh issue list --state open --json number,title,labels,milestone,assignees --limit 100 | \
jq -r 'group_by(.milestone.title // "No Milestone") | .[] |
"## \(.[0].milestone.title // "No Milestone")\n" +
(map("- #\(.number) \(.title) [\(.labels | map(.name) | join(", "))]") | join("\n")) + "\n"'
```
### Milestone burndown summary
```bash
gh api repos/{owner}/{repo}/milestones --jq '
.[] | select(.state == "open") |
"### \(.title)\n" +
" Open: \(.open_issues) | Closed: \(.closed_issues) | " +
"Progress: \((.closed_issues / ((.open_issues + .closed_issues) | if . == 0 then 1 else . end)) * 100 | floor)%\n" +
" Due: \(.due_on // "No due date")\n"'
```
### Closed issues since last report
```bash
gh issue list --state closed --json number,title,closedAt,labels --limit 50 | \
jq --arg since "2026-03-10T00:00:00Z" '
[.[] | select(.closedAt > $since)] |
map("- #\(.number) \(.title)") | join("\n")'
```
### PR merge activity
```bash
gh pr list --state merged --json number,title,mergedAt --limit 20 | \
jq -r '.[] | "- PR #\(.number) \(.title) (merged \(.mergedAt | split("T")[0]))"'
```
## Sprint Planning
### Move items into a sprint
Assign the current iteration to a batch of issues. First, get the iteration field and current iteration IDs (see "Get project fields" above), then loop:
```bash
# For each issue number in the sprint
for ISSUE in 10 12 15 18; do
# Get issue node ID
ISSUE_ID=$(gh api graphql -f query='
query($owner: String!, $repo: String!, $number: Int!) {
repository(owner: $owner, name: $repo) {
issue(number: $number) { id }
}
}
' -f owner="OWNER" -f repo="REPO" -F number=$ISSUE --jq '.data.repository.issue.id')
# Add to project if not already there
ITEM_ID=$(gh api graphql -f query='
mutation($projectId: ID!, $contentId: ID!) {
addProjectV2ItemById(input: {projectId: $projectId, contentId: $contentId}) {
item { id }
}
}
' -f projectId="PROJECT_ID" -f contentId="$ISSUE_ID" --jq '.data.addProjectV2ItemById.item.id')
# Set iteration
gh api graphql -f query='
mutation($projectId: ID!, $itemId: ID!, $fieldId: ID!, $iterationId: String!) {
updateProjectV2ItemFieldValue(input: {
projectId: $projectId
itemId: $itemId
fieldId: $fieldId
value: {iterationId: $iterationId}
}) {
projectV2Item { id }
}
}
' -f projectId="PROJECT_ID" -f itemId="$ITEM_ID" -f fieldId="ITERATION_FIELD_ID" -f iterationId="CURRENT_ITERATION_ID"
done
```
### Prioritization
Apply priority labels and sort within the project:
```bash
# Set priority labels on issues
gh issue edit 10 --add-label "priority: high"
gh issue edit 12 --add-label "priority: medium"
gh issue edit 15 --add-label "priority: low"
```
### Capacity check
Count issues assigned per sprint to avoid overloading:
```bash
gh issue list --label "sprint:current" --state open --json number,title,assignees | \
jq 'length as $total | "Sprint items: \($total)"'
```
### Sprint review checklist
Generate a checklist of done/not-done items for sprint review:
```bash
gh issue list --label "sprint:current" --state all --json number,title,state | \
jq -r '.[] |
(if .state == "CLOSED" then "- [x]" else "- [ ]" end) +
" #\(.number) \(.title)"'
```
## Practical Patterns
### Full workflow: create milestone, label issues, add to project
```bash
OWNER=$(gh repo view --json owner -q .owner.login)
REPO=$(gh repo view --json name -q .name)
# 1. Create milestone
gh api repos/$OWNER/$REPO/milestones -f title="v0.3.0" -f due_on="2026-05-01T00:00:00Z"
# 2. Get milestone number
MILESTONE=$(gh api repos/$OWNER/$REPO/milestones --jq '.[] | select(.title=="v0.3.0") | .number')
# 3. Assign issues and label them
for ISSUE in 20 21 22; do
gh api repos/$OWNER/$REPO/issues/$ISSUE -X PATCH -F milestone=$MILESTONE
gh issue edit $ISSUE --add-label "priority: medium"
done
```
### Error handling
Always check for errors in GraphQL responses:
```bash
RESULT=$(gh api graphql -f query='...' 2>&1)
if echo "$RESULT" | jq -e '.errors' > /dev/null 2>&1; then
echo "GraphQL error: $(echo "$RESULT" | jq -r '.errors[0].message')"
fi
```
Attribution
Comments
Loading comments…