Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions docs/README.skills.md
Original file line number Diff line number Diff line change
Expand Up @@ -216,6 +216,7 @@ See [CONTRIBUTING.md](../CONTRIBUTING.md#adding-skills) for guidelines on how to
| [github-codespaces-efficiency](../skills/github-codespaces-efficiency/SKILL.md)<br />`gh skills install github/awesome-copilot github-codespaces-efficiency` | Audit and improve GitHub Codespaces efficiency. Use this skill when a user wants faster Codespaces startup, lower Codespaces spend, slim devcontainers, right-size machines, tune idle timeout, or scope prebuilds to branches with sustained usage. | `references/codespaces.md`<br />`references/review-rubric.md` |
| [github-copilot-starter](../skills/github-copilot-starter/SKILL.md)<br />`gh skills install github/awesome-copilot github-copilot-starter` | Set up complete GitHub Copilot configuration for a new project based on technology stack | None |
| [github-issues](../skills/github-issues/SKILL.md)<br />`gh skills install github/awesome-copilot github-issues` | Create, update, and manage GitHub issues using MCP tools. Use this skill when users want to create bug reports, feature requests, or task issues, update existing issues, add labels/assignees/milestones, set issue fields (dates, priority, custom fields), set issue types, manage issue workflows, link issues, add dependencies, or track blocked-by/blocking relationships. Triggers on requests like "create an issue", "file a bug", "request a feature", "update issue X", "set the priority", "set the start date", "link issues", "add dependency", "blocked by", "blocking", or any GitHub issue management task. | `references/dependencies.md`<br />`references/images.md`<br />`references/issue-fields.md`<br />`references/issue-types.md`<br />`references/milestones.md`<br />`references/projects.md`<br />`references/search.md`<br />`references/sub-issues.md`<br />`references/templates.md` |
| [github-projects](../skills/github-projects/SKILL.md)<br />`gh skills install github/awesome-copilot github-projects` | Create and administer GitHub Projects (v2) boards via gh CLI and GraphQL β€” project creation, single-select fields and options, views, workflows, and idempotent issue seeding. Covers the evergreen-board pattern (releases as a field, not per-release projects) and the API walls that trip agents up: input-key casing, required option color+description, full-replace option semantics, view/grouping read-only limits, and workflow automation being UI-only. Triggers on "set up a project board", "create a release view", "add issues to a project", "seed a project", "rename a board field", or any board-admin task beyond adding/updating items. | `scripts/seed-project.mjs` |
| [github-release](../skills/github-release/SKILL.md)<br />`gh skills install github/awesome-copilot github-release` | Guides IA through releasing a new version of a GitHub library end-to-end. Handles SemVer versioning and Keep a Changelog formatting automatically. | `references/commit-classification.md`<br />`references/semver-rules.md` |
| [gitmoji](../skills/gitmoji/SKILL.md)<br />`gh skills install github/awesome-copilot gitmoji` | Generates commit messages following the gitmoji convention (https://gitmoji.dev) β€” picks the right emoji for the intent of the change and writes a well-formed message. Use when asked to "write a gitmoji commit", "add an emoji to my commit message", "which gitmoji should I use", "gitmoji this change", or when a project uses gitmoji-style commit messages. Works from a git diff, staged changes, or a plain description of the change. Generates the message only β€” does not run git commands. | `references/gitmoji-reference.md` |
| [go-mcp-server-generator](../skills/go-mcp-server-generator/SKILL.md)<br />`gh skills install github/awesome-copilot go-mcp-server-generator` | Generate a complete Go MCP server project with proper structure, dependencies, and implementation using the official github.com/modelcontextprotocol/go-sdk. | None |
Expand Down
110 changes: 110 additions & 0 deletions skills/github-projects/SKILL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
---
name: github-projects
description: 'Create and administer GitHub Projects (v2) boards via gh CLI and GraphQL β€” project creation, single-select fields and options, views, workflows, and idempotent issue seeding. Covers the evergreen-board pattern (releases as a field, not per-release projects) and the API walls that trip agents up: input-key casing, required option color+description, full-replace option semantics, view/grouping read-only limits, and workflow automation being UI-only. Triggers on "set up a project board", "create a release view", "add issues to a project", "seed a project", "rename a board field", or any board-admin task beyond adding/updating items.'
---

# GitHub Projects (v2) board administration

For adding/updating **items** on an existing board, the `github-issues`
skill's Projects V2 reference is often enough. This skill covers
**administering the board itself**.

## Model: evergreen board, releases as fields

- ONE long-lived org project per product. A release is a `Release`
single-select OPTION + a filtered view β€” NOT a per-release project.
- Suggested fields: `Status` (Todo / In Progress / In Review / Done),
`Release` (one option per cycle), plus one grouping field with
human-readable options (workstream, area, epic β€” readable names, not
plan-document codes).
- Mutations need the `project` scope: `gh auth refresh -s project`
(default token usually has only `read:project` β†’ `INSUFFICIENT_SCOPES`).

## Verified API walls (live-tested, 2026-08)

1. **Input keys are camelCase and inconsistent per mutation:**
- `updateProjectV2Field(input: { fieldId, name?, singleSelectOptions? })`
β€” `fieldId`; no projectId.
- `createProjectV2View(input: { projectId, name, layout })` β€” `projectId`.
- CLI `gh project item-edit` requires BOTH `--project-id` and `--id`.
2. **Every singleSelectOption requires name, color, AND description.**
Valid colors (introspected enum): GRAY, BLUE, GREEN, YELLOW, ORANGE,
RED, PINK, PURPLE. No TEAL/MAGENTA.
3. **`singleSelectOptions` REPLACES the full option list** (empty input is
ignored, not cleared). To merge: read current options first, send the
merged set. Renaming an option while keeping item stamps stable: send
the same names (option IDs are preserved for unchanged names).
4. **View grouping/filter/sort are read-only via API** (community
discussion 153532). Create views with layout only
(`BOARD_LAYOUT` / `TABLE_LAYOUT` / `ROADMAP_LAYOUT`) and set group-by /
filter in the web UI β€” a one-time manual step.
5. **Workflows (board automation) are read + delete via API only** β€”
`updateProjectV2Workflow` does not exist (schema-introspected).
Auto-add issues, "item added β†’ set Status", and close-archiving are
configured in the project's Workflows settings (web UI, one-time).

## CLI fast paths

```bash
# create org-owned project
gh project create --owner ORG --title NAME --format json

# inspect fields + option IDs
gh project field-list N --owner ORG --format json \
--jq '.fields[] | {name, id, options}'

# create a single-select field
gh project field-create N --owner ORG --name Release \
--data-type SINGLE_SELECT --single-select-options "v1.0.0,v1.1.0"

# rename field / replace options (GraphQL; full-replace semantics!)
gh api graphql -f query='mutation { updateProjectV2Field(input: {
fieldId: "PVTSSF_…"
name: "Workstream"
singleSelectOptions: [
{name: "Frontend", color: BLUE, description: "UI work"},
{name: "Security", color: RED, description: "Hardening"}
]
}) { projectV2Field { ... on ProjectV2SingleSelectField { options { id name } } } } }'

# create views (layout only β€” group-by/filter are UI-only)
gh api graphql -f query='mutation { createProjectV2View(input: {
projectId: "PVT_…", name: "vX Board", layout: BOARD_LAYOUT
}) { projectV2View { id name } } }'

# add + stamp items (idempotent add; item-edit prints nothing on success)
gh project item-add N --owner ORG \
--url https://github.com/ORG/REPO/issues/42 --format json
gh project item-edit --project-id PVT_… --id PVTI_… \
--field-id PVTSSF_… --single-select-option-id OPTION_ID

# read board workflows (enabled state)
gh api graphql -f query='{ organization(login: "ORG") { projectV2(number: N) {
workflows(first: 10) { nodes { id name enabled } } } } }' \
--jq '.data.organization.projectV2.workflows.nodes[]'
```

## Idempotent seeding pattern

`addProjectV2ItemById` returns the existing item on re-add, so seed
scripts are safe to re-run per release cycle. The bundled reference
script resolves field/option IDs dynamically each run (never hardcode
option IDs across cycles) and stamps `Release` + `Status` + grouping
field per issue: see [scripts/seed-project.mjs](scripts/seed-project.mjs).

## One-time manual UI steps (API cannot do these)

- Project β†’ Workflows: enable **Auto-add issues** (filter: your repo),
**Item added β†’ Status = Todo**, optionally close-archiving.
- Per view: group by `Status` / grouping field, filter `Release = vX`.

## Common failure modes

| Symptom | Cause |
|---|---|
| `missing required scopes [project]` | Token lacks write scope β†’ `gh auth refresh -s project` |
| `Argument 'fieldId' … required` on field update | Used `projectID` key β€” the field mutations take `fieldId` |
| `invalid value (TEAL)` | Color enum has only GRAY/BLUE/GREEN/YELLOW/ORANGE/RED/PINK/PURPLE |
| Options vanished after edit | `singleSelectOptions` replaces the whole list β€” merge, don't send partial |
| `project-id must be provided` | `gh project item-edit` needs `--project-id` alongside `--id` |
| Empty JSON error when scripting `item-edit` | Success prints nothing β€” don't parse its stdout |
115 changes: 115 additions & 0 deletions skills/github-projects/scripts/seed-project.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
#!/usr/bin/env node
/**
* Seed a GitHub Project (v2) for a release cycle β€” reference pattern.
*
* Idempotent: addProjectV2ItemById returns the existing item on re-add,
* and field stamps are upserts β€” safe to re-run every cycle.
*
* Usage:
* node scripts/seed-project.mjs vX.Y.Z
* PROJECT_OWNER=myorg PROJECT_NUMBER=3 node scripts/seed-project.mjs vX.Y.Z
*
* Requires gh CLI authenticated with the `project` scope
* (gh auth refresh -s project).
*
* Per-cycle edit: bump the ISSUE_MAP below (and add the new Release
* option + any new grouping options to the project fields FIRST β€”
* updateProjectV2Field replaces the full option list, so merge).
*/
import { execFileSync } from 'node:child_process';

const OWNER = process.env.PROJECT_OWNER ?? 'ORG_NAME';
const PROJECT_NUMBER = Number(process.env.PROJECT_NUMBER ?? 1);
const REPO = process.env.SEED_REPO ?? 'ORG_NAME/REPO_NAME';
const RELEASE = process.argv[2] ?? 'vX.Y.Z';

/** issue number β†’ grouping-field option name ('' = leave unstamped) */
const ISSUE_MAP = {
// 18: 'Growth/SEO',
// 19: 'Security',
};

const GROUPING_FIELD = 'Workstream'; // rename to your grouping field
const DEFAULT_STATUS = 'Todo';
// ──────────────────────────────────────────────────────────────────────

const gh = (args, { optionalJson = false } = {}) => {
const out = execFileSync('gh', args, {
encoding: 'utf8',
maxBuffer: 10 * 1024 * 1024,
});
if (!out.trim()) {
if (optionalJson) return null;
throw new Error(`gh ${args.join(' ')} returned empty output`);
}
return JSON.parse(out);
};

const fail = (msg) => {
console.error(`βœ— ${msg}`);
process.exit(1);
};

// 1. Resolve project + fields dynamically (never hardcode option IDs)
const project = gh([
'project', 'view', String(PROJECT_NUMBER),
'--owner', OWNER, '--format', 'json',
]);
const projectId = project.id;
console.log(`project #${PROJECT_NUMBER} β†’ ${projectId} (${project.url})`);

const fields = gh([
'project', 'field-list', String(PROJECT_NUMBER),
'--owner', OWNER, '--format', 'json',
]).fields;

const fieldId = (name) => {
const f = fields.find((x) => x.name === name);
if (!f) fail(`field "${name}" not found. Have: ${fields.map((x) => x.name).join(', ')}`);
return f.id;
};
const optionId = (fieldName, optionName) => {
const f = fields.find((x) => x.name === fieldName);
const o = f?.options?.find((x) => x.name === optionName);
if (!o) fail(`option "${optionName}" not on field "${fieldName}" β€” add it first (updateProjectV2Field REPLACES the option list; merge).`);
return o.id;
};

const releaseField = fieldId('Release');
const statusField = fieldId('Status');
const autoStatus = fields.some((x) => x.name === 'Status');
if (!releaseField || !autoStatus) fail('Release and Status fields must exist on the project first.');

// 2. Add + stamp issues
for (const [num, group] of Object.entries(ISSUE_MAP)) {
const url = `https://github.com/${REPO}/issues/${num}`;
const item = gh([
'project', 'item-add', String(PROJECT_NUMBER),
'--owner', OWNER, '--url', url, '--format', 'json',
]);
const itemId = item.id;

gh([
'project', 'item-edit', '--project-id', projectId, '--id', itemId,
'--field-id', releaseField,
'--single-select-option-id', optionId('Release', RELEASE),
], { optionalJson: true });
gh([
'project', 'item-edit', '--project-id', projectId, '--id', itemId,
'--field-id', statusField,
'--single-select-option-id', optionId('Status', DEFAULT_STATUS),
], { optionalJson: true });
if (group) {
gh([
'project', 'item-edit', '--project-id', projectId, '--id', itemId,
'--field-id', fieldId(GROUPING_FIELD),
'--single-select-option-id', optionId(GROUPING_FIELD, group),
], { optionalJson: true });
}
console.log(`βœ“ #${num} β†’ ${RELEASE}${group ? ` Β· ${group}` : ''}`);
}

console.log(`\nDone: ${Object.keys(ISSUE_MAP).length} items seeded into ${project.url}`);
console.log('One-time manual UI steps (API is read-only for these):');
console.log(' - Workflows: Auto-add issues; Item added β†’ Status=Todo');
console.log(' - Board view: group by Status, filter Release=vX; Backlog view: group by grouping field');
Loading