diff --git a/.claude/skills/pr-roadmap-link/SKILL.md b/.claude/skills/pr-roadmap-link/SKILL.md index 1cc3541..2125956 100644 --- a/.claude/skills/pr-roadmap-link/SKILL.md +++ b/.claude/skills/pr-roadmap-link/SKILL.md @@ -5,10 +5,12 @@ description: Lock a StarStats roadmap-item slug onto a PR's body + label BEFORE # pr-roadmap-link -You're the last line of defense between "ship a feature" and "telemetry knows what feature shipped." The release-promote.mjs script reads `Roadmap-Item: ` from the tag annotation — but that annotation is set per-release by the operator typing `--roadmap-item-slug ` on the promote command. If the slug decision wasn't made when the PR was merged, the operator has to reconstruct it later from git history, which is fragile. +You're the last line of defense between "ship a feature" and "telemetry knows what feature shipped." The release-promote.mjs script's `discoverSlugFromMergedPrs` reads `roadmap/` labels on PRs merged in the release window, so the slug travels with the PR rather than needing operator memory at release time. Your job: at PR-create time, plant the slug on the PR (both as a body trailer and as a `roadmap/` label per spec §3.3) so the operator can read it off the PR when they later cut a release. +**Critical distinction — the `roadmap/` label is the canonical signal.** `release-promote.mjs` auto-discovery reads PR labels only, NOT body trailers. The `Roadmap-Item: ` trailer is human-readable metadata for future-you reading the merged PR; it does not drive any automation. Always plant BOTH (this skill does that for you), but if anyone manually edits a PR later, the LABEL is the load-bearing one. Documented at `docs/RELEASING.md §11.4`. + ## When to fire Fire when ALL of these are true: diff --git a/docs/RELEASING.md b/docs/RELEASING.md index 25708d5..3d61efc 100644 --- a/docs/RELEASING.md +++ b/docs/RELEASING.md @@ -367,16 +367,25 @@ gh workflow run promote.yml \ Inputs: -| Input | Type | Required | Notes | -|-----------|----------|----------|----------------------------------------------------| -| `channel` | choice | yes | `alpha`, `beta`, `rc`, `live` | -| `sha` | string | no | Target SHA on `next`. Defaults to `next` HEAD. | -| `n` | string | no | Explicit pre-release N. Must advance forward. | -| `dry_run` | boolean | no | Prints actions without pushing (default: `false`). | +| Input | Type | Required | Notes | +|---------------------|----------|----------|---------------------------------------------------------------------------------------------| +| `channel` | choice | yes | `alpha`, `beta`, `rc`, `live` | +| `sha` | string | no | Target SHA on `next`. Defaults to `next` HEAD. | +| `n` | string | no | Explicit pre-release N. Must advance forward. | +| `dry_run` | boolean | no | Prints actions without pushing (default: `false`). | +| `roadmap_item_slug` | string | no | Explicit slug for the tag annotation. Overrides auto-discovery from merged PR labels (§11). | `channel=live` runs both a dry-run preview and the real promotion, gated by the `production-release` Environment between them. +The `roadmap_item_slug` input is usually unnecessary because the +release-promote script auto-discovers the slug from `roadmap/` +labels on PRs merged since the previous track tag — see **§11 Roadmap +tracking pipeline** for the full mechanism. Pass `roadmap_item_slug` +when you want to override auto-discovery (e.g., bulk releases that +ship multiple roadmap items, or releases where the labels weren't +applied at PR-create time). + ### 6.3. Automatic alpha cadence Every push to `next` fires the workflow, which: @@ -702,3 +711,124 @@ Three behaviour changes from the pre-2026-05-23 model: Detailed examples for the new subcommands live in the body of this document with the legacy single-track examples flagged where they need a track argument added. + +--- + +## 11. Roadmap tracking pipeline + +Shipped 2026-05-27 → 2026-05-29 (PRs #112, #113, #125, #126, #128, +#129, #130, #133). Closes the loop between "this PR ships feature X" +and "the tray 'What's new' panel publishes when X reaches a channel" +with zero operator typing between feature start and tray notification. + +### 11.1. The six-step chain + +1. **Feature start** — `roadmap-tracking` skill + (`.claude/skills/roadmap-tracking/SKILL.md`) fires post-brainstorming, + pre-TDD. Asks "link existing item / create new / skip tracking" and + stashes the decision at `.claude/session-roadmap-slug`. + +2. **PR create** — `pr-roadmap-link` skill + (`.claude/skills/pr-roadmap-link/SKILL.md`) fires when about to + `gh pr create --base next`. Plants two attribution marks per spec §3.3: + - **`roadmap/` label on the PR** — **canonical**. Auto-discovery + reads this. + - `Roadmap-Item: ` trailer in the PR body — human-readable + only. NOT read by any automation; future-you reading the merged + PR can see the slug without clicking through to labels. + +3. **Release time** — `scripts/release-promote.mjs` auto-discovers the + slug from `roadmap/*` labels on PRs merged in the range `(prev_tag, + target_sha]`. Resolution priority: + 1. `--roadmap-item-slug X` explicit flag wins. + 2. `--no-auto-slug` flag → skip discovery, tag unannotated. + 3. Auto-discover (default): 0 slugs → no annotation; 1 → use it; + 2+ → refuse, demand explicit choice (so multi-slug releases + can't silently mis-attribute). + +4. **Tag annotation** — release-promote writes + `Roadmap-Item: ` into the annotated tag body. The release + workflow's "Resolve roadmap item slug" step parses it via + `git tag -l --format='%(contents)'`. + +5. **CI emit** — `scripts/roadmap-emit-event.mjs` POSTs a signed CI + event (HMAC-SHA256 over `v1..`) to + `POST /v1/internal/roadmap/events`. Server creates a draft + changelog entry on the channel's first-time-Shipped transition. + +6. **CI auto-publish** — `scripts/auto-publish-changelog.mjs` POSTs + another signed request to `POST /v1/internal/roadmap/changelog/publish` + (the HMAC endpoint shipped in #128). Server publishes the draft, + fans out subscriber notifications, fires the Discord webhook, and + the tray "What's new" card surfaces on next refresh. + +Steps 1–4 are operator-facing; 5–6 happen in CI without intervention. + +### 11.2. When to skip tracking + +The `pr-roadmap-link` skill silently skips itself when: + +- Branch name starts with `docs/`, `chore/`, `style/`, `refactor/`, + `release/`, or `revert/`. +- All commits use Conventional Commits prefixes that don't ship + user-visible behavior (`chore:`, `docs:`, `style:`, `test:`, `ci:`). +- You say "no roadmap" / "skip tracking" / "untracked" in the same turn. +- PR body already carries a `Roadmap-Item:` trailer (idempotent re-runs). + +A skipped PR contributes no slug to the eventual release's discovery +window. If the release contains only skipped PRs, you'll see +`[track] slug: (none) — N PR(s) since , none with roadmap/* labels` +in the promote output and the tag will be unannotated. That's the +right outcome for infrastructure releases. + +### 11.3. Manual overrides + +```bash +# Override auto-discovery with an explicit slug +node scripts/release-promote.mjs live tray --roadmap-item-slug my-feature + +# Skip discovery entirely (multi-item release, or unlabeled work) +node scripts/release-promote.mjs live tray --no-auto-slug +``` + +The CI workflow has the equivalent input +(`workflow_dispatch.roadmap_item_slug`) — see §6.2. + +### 11.4. Auto-discovery details + +`discoverSlugFromMergedPrs` in `scripts/release-promote.mjs`: + +1. Finds the previous track tag via `previousTrackTagBelow(tagList, + track, currentVersion)` — channel-agnostic but track-scoped. +2. Gets the prev tag's commit date for a coarse `merged:>{date}` + filter on `gh pr list`. +3. Calls `gh pr list --state merged --base next --search merged:>{date} + --json number,mergeCommit,labels,title`. +4. Narrows by exact ancestry: keeps PRs whose `mergeCommit.oid` is + reachable from `targetSha` AND NOT reachable from `prevTag`. +5. Extracts unique `roadmap/` label suffixes via + `parseSlugsFromPrLabels`. + +Failure-tolerant: any gh / git error returns null and the release +proceeds with no annotation. Network issues never block a release. + +### 11.5. Server-side dependencies + +- HMAC key: `ROADMAP_CI_EVENT_HMAC_KEY` (shared between emit + publish + endpoints — one secret, one auth scheme). +- API base: `STARSTATS_API_URL` (shared with the existing admin-CLI + publish script; one secret per environment, paths appended in code). +- Webhook subscription: the org-level GitHub webhook MUST be + subscribed to BOTH `Projects v2 item` AND `Issues` events. Without + the Issues subscription, label changes on Issues don't propagate to + the server until the 5-min reconciler tick (see PR #112). + +### 11.6. Operator quick reference + +| Symptom | Where to look | +|---|---| +| Release shipped, no changelog drafted | Tag annotation missing `Roadmap-Item:`? Check `git tag -l --format='%(contents)' `. | +| Release shipped, draft created, not published | Auto-publish job log. `ROADMAP_ITEM_SLUG: ` empty → slug not in tag annotation. `[auto-publish] no-op: STARSTATS_API_URL not set` → secret missing. | +| Auto-discovery picked wrong slug | Multiple labeled PRs in range. Pass `--roadmap-item-slug X` explicitly. | +| Auto-discovery picked NO slug | Check `gh pr list --base next --search "merged:>{date}" --json labels` against the prev-tag's date. If labels are missing on PRs, the `pr-roadmap-link` skill didn't fire (or skipped per §11.2). | +| Label exists on PR but discovery still misses | Verify `gh` CLI is on PATH for the release-promote script. Auto-discovery skips silently on `gh` errors. |