From 07a4ae100411a26c6f0cf465054f67b0cb3c304a Mon Sep 17 00:00:00 2001 From: Akhil Date: Sun, 16 Aug 2026 17:17:40 -0500 Subject: [PATCH] =?UTF-8?q?feat:=20add=20github-projects=20skill=20?= =?UTF-8?q?=E2=80=94=20Project=20v2=20board=20administration=20via=20gh=20?= =?UTF-8?q?CLI?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Covers board administration beyond item ops: project creation, the evergreen-board pattern (releases as a Release field option + filtered view, not per-release projects), single-select field/option management, view creation, and idempotent release-cycle seeding with a bundled reference script. Documents API walls verified live against the GraphQL schema: - camelCase input keys that differ per mutation (fieldId vs projectId) - every singleSelectOption requires name + color + description (full color enum included) - singleSelectOptions replaces the full option list — merge semantics - view grouping/filter and workflow automation are UI-only (no updateProjectV2Workflow in schema) - gh project item-edit needs --project-id AND --id; prints empty stdout on success (scripting pitfall) Complements the github-issues skill's Projects V2 reference, which covers item add/update but not board administration. --- docs/README.skills.md | 1 + skills/github-projects/SKILL.md | 110 +++++++++++++++++ .../github-projects/scripts/seed-project.mjs | 115 ++++++++++++++++++ 3 files changed, 226 insertions(+) create mode 100644 skills/github-projects/SKILL.md create mode 100644 skills/github-projects/scripts/seed-project.mjs diff --git a/docs/README.skills.md b/docs/README.skills.md index 75d2269e6c..e10dab98f2 100644 --- a/docs/README.skills.md +++ b/docs/README.skills.md @@ -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)
`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`
`references/review-rubric.md` | | [github-copilot-starter](../skills/github-copilot-starter/SKILL.md)
`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)
`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`
`references/images.md`
`references/issue-fields.md`
`references/issue-types.md`
`references/milestones.md`
`references/projects.md`
`references/search.md`
`references/sub-issues.md`
`references/templates.md` | +| [github-projects](../skills/github-projects/SKILL.md)
`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)
`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`
`references/semver-rules.md` | | [gitmoji](../skills/gitmoji/SKILL.md)
`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)
`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 | diff --git a/skills/github-projects/SKILL.md b/skills/github-projects/SKILL.md new file mode 100644 index 0000000000..177734bff1 --- /dev/null +++ b/skills/github-projects/SKILL.md @@ -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 | diff --git a/skills/github-projects/scripts/seed-project.mjs b/skills/github-projects/scripts/seed-project.mjs new file mode 100644 index 0000000000..53b718f7c2 --- /dev/null +++ b/skills/github-projects/scripts/seed-project.mjs @@ -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');