diff --git a/.github/workflows/pm-auto-archive-closed-pr.yml b/.github/workflows/pm-auto-archive-closed-pr.yml new file mode 100644 index 0000000..1669a1b --- /dev/null +++ b/.github/workflows/pm-auto-archive-closed-pr.yml @@ -0,0 +1,84 @@ +name: pm — auto-archive closed PRs in project + +# When a PR closes (merged or not), archive its project board item immediately. +# Built-in "Auto-archive items" workflow only archives by age (30+ days closed), +# which leaves the active views cluttered with freshly-closed PRs. This Action +# archives on close so the board stays focused on in-flight + open work. +# +# Required repo secret: PM_PROJECT_TOKEN (same as the other pm-* workflows) + +on: + pull_request: + types: [closed] + workflow_dispatch: + inputs: + pr_number: + description: 'PR number to archive (for manual re-runs)' + required: false + +permissions: + contents: read + +jobs: + archive: + runs-on: ubuntu-latest + env: + GH_TOKEN: ${{ secrets.PM_PROJECT_TOKEN }} + PROJECT_OWNER: litentry + PROJECT_NUMBER: '19' + steps: + - name: Install jq + run: sudo apt-get update && sudo apt-get install -y jq + + - name: Determine PR number + id: pr + run: | + if [ "${{ github.event_name }}" = "pull_request" ]; then + echo "number=${{ github.event.pull_request.number }}" >> "$GITHUB_OUTPUT" + else + echo "number=${{ github.event.inputs.pr_number }}" >> "$GITHUB_OUTPUT" + fi + + - name: Resolve project ID + PR item ID + id: resolve + run: | + project_id=$(gh project view "$PROJECT_NUMBER" --owner "$PROJECT_OWNER" --format json | jq -r '.id') + echo "project_id=$project_id" >> "$GITHUB_OUTPUT" + + pr_num="${{ steps.pr.outputs.number }}" + item_id=$(gh api graphql -f query=' + query($owner: String!, $number: Int!) { + organization(login: $owner) { + projectV2(number: $number) { + items(first: 100, orderBy: {field: POSITION, direction: ASC}) { + nodes { + id + content { ... on PullRequest { number } } + } + } + } + } + } + ' -F "owner=$PROJECT_OWNER" -F "number=$PROJECT_NUMBER" \ + | jq -r --arg n "$pr_num" '.data.organization.projectV2.items.nodes[] | select(.content.number == ($n|tonumber)) | .id' \ + | head -n1) + + if [ -z "$item_id" ] || [ "$item_id" = "null" ]; then + echo "info PR #$pr_num is not on the project board — nothing to archive" + echo "found=false" >> "$GITHUB_OUTPUT" + else + echo "item_id=$item_id" >> "$GITHUB_OUTPUT" + echo "found=true" >> "$GITHUB_OUTPUT" + fi + + - name: Archive the PR's project item + if: steps.resolve.outputs.found == 'true' + run: | + gh api graphql -f query=' + mutation($project: ID!, $item: ID!) { + archiveProjectV2Item(input: { projectId: $project, itemId: $item }) { + item { id } + } + } + ' -F "project=${{ steps.resolve.outputs.project_id }}" -F "item=${{ steps.resolve.outputs.item_id }}" \ + >/dev/null && echo "ok archived PR #${{ steps.pr.outputs.number }} from project board" diff --git a/pm/PROJECT-DASHBOARD-GUIDE.md b/pm/PROJECT-DASHBOARD-GUIDE.md index 36d0cae..0438829 100644 --- a/pm/PROJECT-DASHBOARD-GUIDE.md +++ b/pm/PROJECT-DASHBOARD-GUIDE.md @@ -22,10 +22,12 @@ GitHub's Projects v2 API has **specific limits**. Knowing what's automatable up | Set Status on add / close / PR-merge | ✅ | Built-in workflows (Item added / Item closed / Pull request merged — all enabled) | | Auto-close issue when Status=Done | ✅ | Built-in "Auto-close issue" workflow | | Link PR to issue | ✅ | Built-in "Pull request linked to issue" workflow | -| Sync `priority/p*` + `phase/v*` labels → fields | ✅ | `.github/workflows/pm-sync-fields-from-labels.yml` (this repo) | +| Sync `priority/p*` + `kind/*` labels → Priority/Kind fields | ✅ | `.github/workflows/pm-sync-fields-from-labels.yml` (this repo) | +| Auto-archive closed PRs from the board | ✅ | `.github/workflows/pm-auto-archive-closed-pr.yml` | | Create / configure project fields | ✅ | `pm/scripts/setup-project-fields.sh` | | Audit workflow drift | ✅ | `.github/workflows/pm-workflow-audit.yml` (daily) | | Bulk backfill historical issues | ✅ | `bash pm/scripts/add-to-project.sh` | +| Create a new issue with canonical labels + fields | ✅ | `/agentkeys-issue-create` Claude Code skill | | **Configure a workflow's filter expression** | ❌ | **UI ONLY** — API has no `updateProjectV2Workflow` mutation | | **Configure a workflow's trigger / action** | ❌ | **UI ONLY** — same reason | | **Create or configure custom views (group-by, layout, filters)** | ❌ | **UI ONLY** — no `createProjectV2View` / `updateProjectV2View` mutation exists | @@ -43,13 +45,14 @@ gh auth refresh -s project,read:project # Verify access gh project list --owner litentry | grep "19" -# Create project fields (Priority/Phase/Estimate/Risk/Notes) +# Create project fields (Priority/Kind/Risk/Notes/Iteration/Blocked-by) +# Idempotent: detects existing fields with empty options and rebuilds; cleans "Project X" zombies. bash pm/scripts/setup-project-fields.sh ``` ### Add a CI secret for the GitHub Actions -The 2 PM workflows (`pm-workflow-audit.yml`, `pm-sync-fields-from-labels.yml`) need a token with org-project scopes — the default `GITHUB_TOKEN` does not have them. +The 3 PM workflows (`pm-workflow-audit.yml`, `pm-sync-fields-from-labels.yml`, `pm-auto-archive-closed-pr.yml`) need a token with org-project scopes — the default `GITHUB_TOKEN` does not have them. 1. Create a fine-grained PAT at https://github.com/settings/tokens - Org permissions: **Projects = read & write** @@ -210,16 +213,20 @@ After the board exists: ### Engineer creating new work +**Recommended**: invoke the `/agentkeys-issue-create` Claude Code skill — it walks you through Kind / Priority / Size / Area / Milestone / Blocked-by dropdowns and creates the issue with the right labels + project-field values. + +Direct CLI fallback (project field values must be set separately in the UI): + ```bash -# Just create the issue with the right labels — built-in + GH Action workflows do the rest: -# 1. "Auto-add to project" built-in workflow → adds it to the board with Status=Todo -# 2. pm-sync-fields-from-labels.yml GH Action → mirrors priority/* + phase/* labels into the -# Priority + Phase project fields gh issue create --repo litentry/agentKeys \ - --title "Phase 2: " \ + --title "" \ --body "Scope..." \ --milestone "M2: First vendor wedge (incl memory system)" \ - --label "area/mcp,kind/feature,phase/v2,priority/p2" + --label "area/mcp" + +# Then in the project UI: set Kind, Priority, Size on the new item. +# Or wait for pm-sync-fields-from-labels.yml — it auto-syncs priority/* labels if you +# add them (but priority labels were removed in the migration; field is now primary). ``` For repeatable issue creation (e.g., planning a sprint), prefer the declarative path: diff --git a/pm/README.md b/pm/README.md index b2588e3..226d144 100644 --- a/pm/README.md +++ b/pm/README.md @@ -69,26 +69,33 @@ When you create a new issue via `gh issue create` (or web UI), the milestone/lab Recommended pattern: ```bash -# Create issue with milestone + labels inline +# Recommended path: use the /agentkeys-issue-create skill (interactive, fills all metadata) +# Or directly with gh: gh issue create --repo litentry/agentKeys \ --title "..." --body "..." \ --milestone "M1: First MCP demo + Volcano Ark PoC" \ - --label "area/mcp,kind/feature,priority/p1" + --label "area/mcp" -# Then record it in issue-assignments.json for the next sync to honor +# Then set Kind / Priority / Size in the project UI (or let the skill do it) ``` -## Labels schema +## Labels schema (post-migration) -Five label namespaces. An issue typically has one from each, plus optional extras: +Repo labels are now LEAN. Most categorization moved to project fields. Remaining label namespaces: | Namespace | Examples | Purpose | |---|---|---| -| `area/*` | `area/mcp`, `area/memory`, `area/firmware` | Which subsystem | -| `kind/*` | `kind/feature`, `kind/bug`, `kind/research` | What kind of work | -| `phase/*` | `phase/v0`, `phase/v1`, `phase/v2` | Coarse roadmap phase (orthogonal to milestone for cross-milestone work) | -| `status/*` | `status/ready`, `status/blocked`, `status/investigating`, `status/deprecated` | Workflow state | -| `priority/*` | `priority/p0`, `priority/p1`, `priority/p2`, `priority/p3` | Triage priority | +| `area/*` | `area/mcp`, `area/memory`, `area/firmware` (17 total, distinct colors per area) | Which subsystem — multi-value, renders as repo-list filter | +| `status/*` | `status/ready`, `status/in-progress`, `status/deprecated` (non-red); `status/blocked`, `status/investigating` (red) | Workflow state | +| Human-attention flags (red) | `needs-arch-review`, `needs-investigation`, `vendor-blocker` | Flagged for human follow-up | +| Community labels | `good first issue`, `help wanted` | Community discoverability | + +**Migrated to project fields (no longer labels):** +- `priority/p0..p3` → **Priority field** (Urgent / High / Medium / Low) +- `kind/*` → **Kind field** (Feature / Bug / Research / Docs / Refactor / Security / CI) +- `phase/v*` → **Milestones** (M1..M7) + +**Red is reserved** for human-interaction labels (status/blocked, needs-*, vendor-blocker). Area labels avoid the red family. ## Milestones overview diff --git a/pm/labels.json b/pm/labels.json index 6bb5671..33b0921 100644 --- a/pm/labels.json +++ b/pm/labels.json @@ -1,49 +1,37 @@ { + "_note": "Repo label taxonomy. Kept lean since most categorization moved to project fields (Kind, Priority, Size) — labels here are for cross-cutting flags + repo-list filtering only.", + "_color_convention": "Red family (b60205, d73a4a, dc2626) is RESERVED for labels requiring human attention/intervention. Area labels use a distinct color per functional area, drawn from blue/teal/green/purple families.", + "_migrated_away": "priority/p* (→ Priority project field), phase/v* (→ Milestones), kind/* (→ Kind project field). Removed by setup. To re-introduce any of these, undo via gh label create — but prefer the field over a label.", "labels": [ - { "name": "area/mcp", "color": "0e8a16", "description": "MCP server, MCP tool integration, MCP protocol work" }, - { "name": "area/memory", "color": "0e8a16", "description": "Memory worker, namespaces, semantic/episodic/profile/procedural storage" }, - { "name": "area/identity", "color": "0e8a16", "description": "HDKD actor tree, K-key inventory, identity ceremony" }, - { "name": "area/broker", "color": "0e8a16", "description": "Broker server, cap-token issuance, OIDC issuance" }, - { "name": "area/signer", "color": "0e8a16", "description": "Signer / TEE worker, K3 / K10 / K11 handling" }, - { "name": "area/tee", "color": "0e8a16", "description": "TEE-specific work (signer, attestation, sealing)" }, - { "name": "area/audit", "color": "0e8a16", "description": "Audit worker, two-tier audit (off-chain feed + on-chain anchor)" }, - { "name": "area/credential", "color": "0e8a16", "description": "Credential worker, vault, per-data-class isolation" }, - { "name": "area/payment", "color": "0e8a16", "description": "Payment worker, spending caps, ACP/AMP rail adapters" }, - { "name": "area/ui", "color": "0e8a16", "description": "Parent-control UI, vendor onboarding portal, audit dashboard" }, - { "name": "area/firmware", "color": "0e8a16", "description": "ESP32 firmware, device-side code, MCU work" }, - { "name": "area/ci", "color": "0e8a16", "description": "CI pipelines, GitHub Actions workflows, harness automation" }, - { "name": "area/infra", "color": "0e8a16", "description": "Deployment, broker host, scripts/setup-*.sh, AWS / chain provisioning" }, - { "name": "area/cli", "color": "0e8a16", "description": "agentkeys CLI, operator workstation" }, - { "name": "area/daemon", "color": "0e8a16", "description": "agentkeys-daemon (sidecar) work" }, - { "name": "area/scraper", "color": "0e8a16", "description": "Provisioner scrapers, automation for service signup flows" }, - { "name": "area/docs", "color": "0e8a16", "description": "Documentation, runbooks, architecture, research" }, + { "name": "area/mcp", "color": "1D76DB", "description": "MCP server, MCP tool integration, MCP protocol work" }, + { "name": "area/memory", "color": "5319E7", "description": "Memory worker, namespaces, semantic/episodic/profile/procedural storage" }, + { "name": "area/identity", "color": "8B5CF6", "description": "HDKD actor tree, K-key inventory, identity ceremony" }, + { "name": "area/broker", "color": "006B75", "description": "Broker server, cap-token issuance, OIDC issuance" }, + { "name": "area/signer", "color": "0E8A8C", "description": "Signer / TEE worker, K3 / K10 / K11 handling" }, + { "name": "area/tee", "color": "0E4C7E", "description": "TEE-specific work (signer, attestation, sealing)" }, + { "name": "area/audit", "color": "BFB200", "description": "Audit worker, two-tier audit (off-chain feed + on-chain anchor)" }, + { "name": "area/credential", "color": "0FAA86", "description": "Credential worker, vault, per-data-class isolation" }, + { "name": "area/payment", "color": "0E8A16", "description": "Payment worker, spending caps, ACP/AMP rail adapters" }, + { "name": "area/ui", "color": "C5B0F0", "description": "Parent-control UI, vendor onboarding portal, audit dashboard" }, + { "name": "area/firmware", "color": "5C4033", "description": "ESP32 firmware, device-side code, MCU work" }, + { "name": "area/ci", "color": "94A3B8", "description": "CI pipelines, GitHub Actions workflows, harness automation" }, + { "name": "area/infra", "color": "4A5D23", "description": "Deployment, broker host, scripts/setup-*.sh, AWS / chain provisioning" }, + { "name": "area/cli", "color": "64748B", "description": "agentkeys CLI, operator workstation" }, + { "name": "area/daemon", "color": "2D6A4F", "description": "agentkeys-daemon (sidecar) work" }, + { "name": "area/scraper", "color": "52796F", "description": "Provisioner scrapers, automation for service signup flows" }, + { "name": "area/docs", "color": "0EA5E9", "description": "Documentation, runbooks, architecture, research" }, - { "name": "kind/feature", "color": "a2eeef", "description": "New feature implementation" }, - { "name": "kind/bug", "color": "d73a4a", "description": "Defect; something broken or behaving wrong" }, - { "name": "kind/refactor", "color": "fbca04", "description": "Internal restructuring; no external behavior change" }, - { "name": "kind/research", "color": "ffb760", "description": "Investigation, exploration, prototyping" }, - { "name": "kind/docs", "color": "0075ca", "description": "Documentation-only change" }, - { "name": "kind/security", "color": "b60205", "description": "Security-sensitive — apply extra review rigor" }, - { "name": "kind/devx", "color": "c5def5", "description": "Developer experience, internal tooling, ergonomics" }, - - { "name": "phase/v0", "color": "5319e7", "description": "Already shipped (Stage 7+ era)" }, - { "name": "phase/v1", "color": "5319e7", "description": "Phase 1 work (M1 + immediate follow-ups)" }, - { "name": "phase/v2", "color": "5319e7", "description": "Phase 2-3 work (vendor wedge + runtime neutrality)" }, - { "name": "phase/v3", "color": "5319e7", "description": "Phase 4-5 work (delegation depth + native mobile)" }, - { "name": "phase/v4", "color": "5319e7", "description": "Phase 6-7 work (TEE depth + standards)" }, - - { "name": "status/ready", "color": "0e8a16", "description": "Ready for engineering pickup" }, - { "name": "status/blocked", "color": "d93f0b", "description": "Blocked on external dependency or upstream decision" }, - { "name": "status/investigating", "color": "fbca04", "description": "Under investigation; scope not yet locked" }, - { "name": "status/deprecated", "color": "cfd3d7", "description": "No longer relevant; flagged for close after review" }, + { "name": "status/ready", "color": "0e8a16", "description": "Ready for engineering pickup" }, { "name": "status/in-progress", "color": "1d76db", "description": "Active engineering work in flight" }, + { "name": "status/deprecated", "color": "cfd3d7", "description": "No longer relevant; flagged for close after review" }, - { "name": "priority/p0", "color": "b60205", "description": "Critical — drop other work" }, - { "name": "priority/p1", "color": "d93f0b", "description": "High — this milestone's headline" }, - { "name": "priority/p2", "color": "fbca04", "description": "Medium — important but not blocking" }, - { "name": "priority/p3", "color": "c5def5", "description": "Low — nice to have, can slip" }, + { "name": "status/blocked", "color": "b60205", "description": "Blocked on external dependency or upstream decision — needs human unblock" }, + { "name": "status/investigating", "color": "d73a4a", "description": "Under investigation; needs human follow-up to lock scope" }, + { "name": "needs-arch-review", "color": "dc2626", "description": "Needs explicit arch.md compatibility review before merge" }, + { "name": "needs-investigation", "color": "b60205", "description": "Root cause unclear; assign to someone to investigate" }, + { "name": "vendor-blocker", "color": "b60205", "description": "Blocks a vendor pilot or partnership conversation" }, - { "name": "needs-arch-review", "color": "5319e7", "description": "Needs explicit arch.md compatibility review before merge" }, - { "name": "vendor-blocker", "color": "b60205", "description": "Blocks a vendor pilot or partnership conversation" } + { "name": "good first issue", "color": "7057ff", "description": "Good for newcomers" }, + { "name": "help wanted", "color": "008672", "description": "Extra attention is needed" } ] } diff --git a/pm/scripts/setup-project-fields.sh b/pm/scripts/setup-project-fields.sh index 6ec1064..22621f7 100755 --- a/pm/scripts/setup-project-fields.sh +++ b/pm/scripts/setup-project-fields.sh @@ -48,7 +48,7 @@ existing_fields_json=$(gh api graphql -f query=' # "Project Priority", "Project Project Priority", etc. — clutter that confuses operators # and breaks group-by-field views. Detect + delete any "Project " zombie. cleanup_zombies() { - local managed_names="Priority Phase Estimate Risk Notes" + local managed_names="Priority Kind Phase Estimate Risk Notes" for n in $managed_names; do local zombie_name="Project $n" local zombie_id @@ -135,14 +135,18 @@ create_field() { echo "setup-project-fields target=$PROJECT_OWNER/$PROJECT_NUMBER" -# Priority — single-select, four levels matching priority/* labels -create_field "Priority" SINGLE_SELECT "P0,P1,P2,P3" +# Priority — single-select, mapped from priority/p* labels (p0→Urgent, etc.) +create_field "Priority" SINGLE_SELECT "Urgent,High,Medium,Low" -# Phase — single-select, matches phase/* labels (one phase per issue is the norm) -create_field "Phase" SINGLE_SELECT "v0,v1,v2,v3,v4" +# Kind — single-select, mapped from kind/* labels (one kind per issue) +create_field "Kind" SINGLE_SELECT "Feature,Bug,Research,Docs,Refactor,Security,CI" -# Estimate — t-shirt sizes for rough sizing -create_field "Estimate" SINGLE_SELECT "XS,S,M,L,XL" +# Phase — DEPRECATED. We use GitHub Milestones for phase tracking now. +# The Phase field may still exist on the project; this script leaves it untouched. +# Delete it manually via the UI when ready. + +# Estimate — DEPRECATED. GitHub's built-in Size field (XS/S/M/L/XL) replaces it. +# Leave existing Estimate column untouched if present. # Iteration — sprint window (project's built-in Iteration type; if not supported, # fall back to a TEXT field that operators fill manually). gh CLI doesn't support @@ -156,6 +160,11 @@ create_field "Risk" SINGLE_SELECT "Low,Medium,High,Critical" # Notes — free-form text for one-line context per item create_field "Notes" TEXT +# Blocked by — TEXT, list of "#NN, #MM" issue refs that block this one. We do a +# topological sort manually in views; the field is the source of truth for the +# blocking-graph until GitHub ships a real issue-relationship field type. +create_field "Blocked by" TEXT + echo "" echo "ok setup-project-fields complete" echo "" diff --git a/pm/scripts/sync-fields-from-labels.sh b/pm/scripts/sync-fields-from-labels.sh index 6b00a40..81eb0ce 100755 --- a/pm/scripts/sync-fields-from-labels.sh +++ b/pm/scripts/sync-fields-from-labels.sh @@ -3,8 +3,14 @@ # Mirrors issue labels into project single-select fields. # # Mapping: -# label `priority/p0`..`priority/p3` → Priority field = P0..P3 -# label `phase/v0`..`phase/v4` → Phase field = v0..v4 +# label `priority/p0` → Priority field = Urgent +# label `priority/p1` → Priority field = High +# label `priority/p2` → Priority field = Medium +# label `priority/p3` → Priority field = Low +# label `kind/feature` → Kind field = Feature (case-insensitive match for all kind/* labels) +# label `kind/bug` → Kind field = Bug, etc. +# label `phase/v0`..`phase/v4` → Phase field = v0..v4 (DEPRECATED — milestones replace this; +# kept here for back-compat until Phase field is removed) # # Usage: # bash pm/scripts/sync-fields-from-labels.sh # all open issues in PM_REPO @@ -57,31 +63,36 @@ fields_json=$(gh api graphql -f query=' ' -F "id=$project_id") priority_field_id=$(echo "$fields_json" | jq -r '.data.node.fields.nodes[] | select(.name == "Priority") | .id') -phase_field_id=$(echo "$fields_json" | jq -r '.data.node.fields.nodes[] | select(.name == "Phase") | .id') +kind_field_id=$(echo "$fields_json" | jq -r '.data.node.fields.nodes[] | select(.name == "Kind") | .id') +phase_field_id=$(echo "$fields_json" | jq -r '.data.node.fields.nodes[] | select(.name == "Phase") | .id') # Forgiving mode: if a field is missing, warn + skip syncing that label class # instead of aborting. Operator can add the missing field via setup-project-fields.sh # and re-run; the existing one still gets synced today. if [ -z "$priority_field_id" ] || [ "$priority_field_id" = "null" ]; then - echo "warn Priority field not found — skipping priority/* label sync. Run setup-project-fields.sh to enable." + echo "warn Priority field not found — skipping priority/* label sync." priority_field_id="" fi +if [ -z "$kind_field_id" ] || [ "$kind_field_id" = "null" ]; then + echo "warn Kind field not found — skipping kind/* label sync." + kind_field_id="" +fi if [ -z "$phase_field_id" ] || [ "$phase_field_id" = "null" ]; then - echo "warn Phase field not found — skipping phase/* label sync. Run setup-project-fields.sh to enable." + echo "info Phase field not found — skipping phase/* (expected once Phase is dropped)." phase_field_id="" fi -if [ -z "$priority_field_id" ] && [ -z "$phase_field_id" ]; then - echo "fail neither Priority nor Phase field exists; nothing to sync" +if [ -z "$priority_field_id" ] && [ -z "$kind_field_id" ] && [ -z "$phase_field_id" ]; then + echo "fail no syncable fields exist; nothing to do" exit 1 fi -echo "priority_field_id=${priority_field_id:-} phase_field_id=${phase_field_id:-}" +echo "priority_field_id=${priority_field_id:-} kind_field_id=${kind_field_id:-} phase_field_id=${phase_field_id:-}" # Build label→option-id maps (bash 3.2 compatible: parallel arrays, not associative) -# priority/p0 → P0 option id, etc. priority_options=$(echo "$fields_json" | jq -c '.data.node.fields.nodes[] | select(.name == "Priority") | .options') -phase_options=$(echo "$fields_json" | jq -c '.data.node.fields.nodes[] | select(.name == "Phase") | .options') +kind_options=$(echo "$fields_json" | jq -c '.data.node.fields.nodes[] | select(.name == "Kind") | .options') +phase_options=$(echo "$fields_json" | jq -c '.data.node.fields.nodes[] | select(.name == "Phase") | .options') # Helper: given (label_value, options_json), return option ID matching the value (case-insensitive) option_id_for() { @@ -92,6 +103,17 @@ option_id_for() { echo "$options_json" | jq -r --arg v "$lower" '.[] | select((.name | ascii_downcase) == $v) | .id' | head -n1 } +# Priority needs an explicit mapping (label "p0" → option "Urgent", not a direct name match) +priority_label_to_option_name() { + case "$1" in + p0) echo "Urgent" ;; + p1) echo "High" ;; + p2) echo "Medium" ;; + p3) echo "Low" ;; + *) echo "" ;; + esac +} + # --- Per-issue sync ------------------------------------------------------------ sync_one() { @@ -137,13 +159,43 @@ sync_one() { local labels labels=$(gh issue view "$issue_num" --repo "$REPO" --json labels --jq '.labels[].name' 2>/dev/null || echo "") - # --- Priority ------------------------------------------------------------- + # --- Priority (explicit mapping: p0→Urgent, p1→High, p2→Medium, p3→Low) --- local priority_label priority_label=$(echo "$labels" | grep -E '^priority/' | head -n1 | sed 's|^priority/||' || true) if [ -n "$priority_label" ] && [ -n "$priority_field_id" ]; then - local p_opt - p_opt=$(option_id_for "$priority_label" "$priority_options") - if [ -n "$p_opt" ]; then + local p_option_name + p_option_name=$(priority_label_to_option_name "$priority_label") + if [ -n "$p_option_name" ]; then + local p_opt + p_opt=$(option_id_for "$p_option_name" "$priority_options") + if [ -n "$p_opt" ]; then + gh api graphql -f query=' + mutation($project: ID!, $item: ID!, $field: ID!, $opt: String!) { + updateProjectV2ItemFieldValue(input: { + projectId: $project + itemId: $item + fieldId: $field + value: { singleSelectOptionId: $opt } + }) { projectV2Item { id } } + } + ' -F "project=$project_id" -F "item=$item_id" -F "field=$priority_field_id" -f "opt=$p_opt" \ + >/dev/null && echo "ok #$issue_num Priority=$p_option_name (from priority/$priority_label)" \ + || echo "fail #$issue_num Priority mutation" + else + echo "warn #$issue_num Priority option '$p_option_name' not found in field — re-run setup-project-fields.sh" + fi + else + echo "warn #$issue_num unknown priority label 'priority/$priority_label' (expected p0..p3)" + fi + fi + + # --- Kind (direct case-insensitive match: kind/feature → Feature) --------- + local kind_label + kind_label=$(echo "$labels" | grep -E '^kind/' | head -n1 | sed 's|^kind/||' || true) + if [ -n "$kind_label" ] && [ -n "$kind_field_id" ]; then + local k_opt + k_opt=$(option_id_for "$kind_label" "$kind_options") + if [ -n "$k_opt" ]; then gh api graphql -f query=' mutation($project: ID!, $item: ID!, $field: ID!, $opt: String!) { updateProjectV2ItemFieldValue(input: { @@ -153,15 +205,15 @@ sync_one() { value: { singleSelectOptionId: $opt } }) { projectV2Item { id } } } - ' -F "project=$project_id" -F "item=$item_id" -F "field=$priority_field_id" -f "opt=$p_opt" \ - >/dev/null && echo "ok #$issue_num Priority=$priority_label" \ - || echo "fail #$issue_num Priority mutation" + ' -F "project=$project_id" -F "item=$item_id" -F "field=$kind_field_id" -f "opt=$k_opt" \ + >/dev/null && echo "ok #$issue_num Kind=$kind_label" \ + || echo "fail #$issue_num Kind mutation" else - echo "warn #$issue_num priority label '$priority_label' has no matching field option" + echo "warn #$issue_num kind label 'kind/$kind_label' has no matching field option" fi fi - # --- Phase ----------------------------------------------------------------- + # --- Phase (deprecated; kept for back-compat until field removed) --------- local phase_label phase_label=$(echo "$labels" | grep -E '^phase/' | head -n1 | sed 's|^phase/||' || true) if [ -n "$phase_label" ] && [ -n "$phase_field_id" ]; then